Files
kenaito-kopsc/internal/executor/logsearch.go
T
odboy b4590bbffd feat: 新增 svc.list/log.list 命令
svc.list 列出所有服务(systemctl/launchctl/sc)
log.list 列出可用日志来源(journalctl/日志目录/wevtutil)
2026-11-03 21:12:47 +08:00

297 lines
8.3 KiB
Go

package executor
import (
"bufio"
"context"
"fmt"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
)
// runLogList implements `kopsc log list`: list available log sources.
func runLogList(ctx context.Context, req BuiltinRequest) error {
switch runtime.GOOS {
case "linux":
cmd := exec.CommandContext(ctx, "journalctl", "--list-boots", "--no-pager")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
case "darwin":
fmt.Fprintln(req.Stdout, "macOS 日志目录:")
fmt.Fprintln(req.Stdout, " /var/log - 系统日志")
fmt.Fprintln(req.Stdout, " ~/Library/Logs - 用户日志")
fmt.Fprintln(req.Stdout, " /Library/Logs - 全局日志")
cmd := exec.CommandContext(ctx, "ls", "-la", "/var/log")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
case "windows":
cmd := exec.CommandContext(ctx, "wevtutil", "el")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
default:
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
// runLogSearch implements `kopsc log search <keyword>`: search system logs.
func runLogSearch(ctx context.Context, req BuiltinRequest) error {
opts, err := parseLogSearchArgs(req.Args)
if err != nil {
return err
}
switch runtime.GOOS {
case "linux":
return logSearchLinux(ctx, opts, req)
case "darwin":
return logSearchDarwin(ctx, opts, req)
case "windows":
return logSearchWindows(ctx, opts, req)
default:
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
type logSearchOptions struct {
keyword string
since string
unit string
lines int
}
func parseLogSearchArgs(args []string) (logSearchOptions, error) {
opts := logSearchOptions{lines: 100}
var positional []string
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
positional = append(positional, args[i+1:]...)
i = len(args)
case arg == "--since":
if i+1 >= len(args) {
return opts, fmt.Errorf("--since 缺少时长,例如 --since 1h")
}
i++
opts.since = args[i]
case strings.HasPrefix(arg, "--since="):
opts.since = strings.TrimPrefix(arg, "--since=")
case arg == "-u" || arg == "--unit":
if i+1 >= len(args) {
return opts, fmt.Errorf("%s 缺少服务名", arg)
}
i++
opts.unit = args[i]
case strings.HasPrefix(arg, "--unit="):
opts.unit = strings.TrimPrefix(arg, "--unit=")
case arg == "-n" || arg == "--lines":
if i+1 >= len(args) {
return opts, fmt.Errorf("%s 缺少行数", arg)
}
i++
n, err := strconv.Atoi(args[i])
if err != nil || n < 1 {
return opts, fmt.Errorf("--lines 需要正整数,收到 %q", args[i])
}
opts.lines = n
case strings.HasPrefix(arg, "--lines="):
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--lines="))
if err != nil || n < 1 {
return opts, fmt.Errorf("--lines 需要正整数")
}
opts.lines = n
case strings.HasPrefix(arg, "-") && arg != "-":
return opts, fmt.Errorf("未知选项 %q", arg)
default:
positional = append(positional, arg)
}
}
if len(positional) == 0 {
return opts, fmt.Errorf("缺少搜索关键字;用法: kopsc log search <关键字>")
}
opts.keyword = positional[0]
return opts, nil
}
func logSearchLinux(ctx context.Context, opts logSearchOptions, req BuiltinRequest) error {
args := []string{"--no-pager", "-n", strconv.Itoa(opts.lines), "--grep=" + opts.keyword}
if opts.since != "" {
since, err := normalizeSince(opts.since)
if err != nil {
return fmt.Errorf("--since: %w", err)
}
args = append(args, "--since="+since)
}
if opts.unit != "" {
args = append(args, "-u", opts.unit)
}
cmd := exec.CommandContext(ctx, "journalctl", args...)
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
func logSearchDarwin(ctx context.Context, opts logSearchOptions, req BuiltinRequest) error {
predicate := fmt.Sprintf(`eventMessage CONTAINS[c] "%s"`, opts.keyword)
if opts.unit != "" {
predicate = fmt.Sprintf(`(subsystem == "%s" OR process == "%s") AND %s`, opts.unit, opts.unit, predicate)
}
since := "1h"
if opts.since != "" {
since = opts.since
}
cmd := exec.CommandContext(ctx, "log", "show", "--predicate", predicate, "--last", since, "--style", "compact")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
func logSearchWindows(ctx context.Context, opts logSearchOptions, req BuiltinRequest) error {
args := []string{"qe", "System", "/q:*[System[Provider]]", "/c:" + strconv.Itoa(opts.lines), "/rd:true", "/f:text"}
cmd := exec.CommandContext(ctx, "wevtutil", args...)
var stdout strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = req.Stderr
if err := cmd.Run(); err != nil {
return err
}
scanner := bufio.NewScanner(strings.NewReader(stdout.String()))
count := 0
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(strings.ToLower(line), strings.ToLower(opts.keyword)) {
fmt.Fprintln(req.Stdout, line)
count++
}
}
if count == 0 {
fmt.Fprintf(req.Stdout, "没有找到包含 %q 的日志\n", opts.keyword)
}
return nil
}
// normalizeSince converts shorthand like "1h", "30m" to journalctl --since format.
func normalizeSince(s string) (string, error) {
s = strings.TrimSpace(s)
if s == "" {
return "", fmt.Errorf("时长不能为空")
}
if d, err := time.ParseDuration(s); err == nil {
t := time.Now().Add(-d)
return t.Format("2006-01-02 15:04:05"), nil
}
return s, nil
}
// runLogLast implements `kopsc log last`: show recent system log entries.
func runLogLast(ctx context.Context, req BuiltinRequest) error {
opts, err := parseLogLastArgs(req.Args)
if err != nil {
return err
}
switch runtime.GOOS {
case "linux":
return logLastLinux(ctx, opts, req)
case "darwin":
return logLastDarwin(ctx, opts, req)
case "windows":
return logLastWindows(ctx, opts, req)
default:
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
type logLastOptions struct {
lines int
unit string
}
func parseLogLastArgs(args []string) (logLastOptions, error) {
opts := logLastOptions{lines: 50}
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
remaining := args[i+1:]
if len(remaining) > 0 {
n, err := strconv.Atoi(remaining[0])
if err != nil || n < 1 {
return opts, fmt.Errorf("行数需要正整数,收到 %q", remaining[0])
}
opts.lines = n
}
i = len(args)
case arg == "-n" || arg == "--lines":
if i+1 >= len(args) {
return opts, fmt.Errorf("%s 缺少行数", arg)
}
i++
n, err := strconv.Atoi(args[i])
if err != nil || n < 1 {
return opts, fmt.Errorf("--lines 需要正整数,收到 %q", args[i])
}
opts.lines = n
case strings.HasPrefix(arg, "--lines="):
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--lines="))
if err != nil || n < 1 {
return opts, fmt.Errorf("--lines 需要正整数")
}
opts.lines = n
case arg == "-u" || arg == "--unit":
if i+1 >= len(args) {
return opts, fmt.Errorf("%s 缺少服务名", arg)
}
i++
opts.unit = args[i]
case strings.HasPrefix(arg, "--unit="):
opts.unit = strings.TrimPrefix(arg, "--unit=")
case strings.HasPrefix(arg, "-") && arg != "-":
return opts, fmt.Errorf("未知选项 %q", arg)
default:
n, err := strconv.Atoi(arg)
if err != nil || n < 1 {
return opts, fmt.Errorf("行数需要正整数,收到 %q", arg)
}
opts.lines = n
}
}
return opts, nil
}
func logLastLinux(ctx context.Context, opts logLastOptions, req BuiltinRequest) error {
args := []string{"--no-pager", "-n", strconv.Itoa(opts.lines)}
if opts.unit != "" {
args = append(args, "-u", opts.unit)
}
cmd := exec.CommandContext(ctx, "journalctl", args...)
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
func logLastDarwin(ctx context.Context, opts logLastOptions, req BuiltinRequest) error {
predicate := `eventMessage != ""`
if opts.unit != "" {
predicate = fmt.Sprintf(`subsystem == "%s" OR process == "%s"`, opts.unit, opts.unit)
}
cmd := exec.CommandContext(ctx, "log", "show", "--predicate", predicate, "--last", "5m", "--style", "compact")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
func logLastWindows(ctx context.Context, opts logLastOptions, req BuiltinRequest) error {
cmd := exec.CommandContext(ctx, "wevtutil", "qe", "System",
"/c:"+strconv.Itoa(opts.lines), "/rd:true", "/f:text")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}