Files
weichenandCursor bed0dbf076
ci / vet-and-test (push) Successful in 4s
ci / docker-checkout-hostgateway (push) Successful in 2s
Add a structured logger and collector so runs can be traced across processes.
Sync and async sinks, redaction, and a queryable HTTP service keep concurrent logs tied to a trace id.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:51:16 +00:00

175 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package cli 实现 logctl:serve / tail / query。
package cli
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"logging-system/server"
)
// Main 执行命令行并返回退出码。
func Main(args []string) int { return Run(args, os.Stdout, os.Stderr) }
// Run 执行命令。
func Run(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(stderr, usage)
return 2
}
switch args[0] {
case "serve":
return cmdServe(args[1:], stderr)
case "query":
return cmdQuery(args[1:], stdout, stderr)
case "tail":
return cmdTail(args[1:], stdout, stderr)
case "-h", "--help", "help":
fmt.Fprintln(stdout, usage)
return 0
default:
fmt.Fprintf(stderr, "未知命令 %q\n%s\n", args[0], usage)
return 2
}
}
const usage = `用法:
logctl serve [--addr :8092] [--data 文件] [--token 密钥]
logctl query --addr URL [--trace ID] [--level 级别] [--service 名] [--q 关键词] [--token 密钥]
logctl tail --addr URL [--trace ID] [--token 密钥]`
func cmdServe(args []string, stderr io.Writer) int {
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
fs.SetOutput(stderr)
addr := fs.String("addr", server.DefaultAddr, "监听地址")
data := fs.String("data", "", "JSONL 落盘路径,空则只放内存")
token := fs.String("token", "", "可选写入令牌")
if err := fs.Parse(args); err != nil {
return 2
}
srv, err := server.New(*data, *token, 0)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
defer srv.Close()
fmt.Fprintf(stderr, "logctl serve %s\n", *addr)
if err := srv.ListenAndServe(*addr); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
return 0
}
func cmdQuery(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("query", flag.ContinueOnError)
fs.SetOutput(stderr)
addr := fs.String("addr", "http://127.0.0.1:8092", "采集器地址")
traceID := fs.String("trace", "", "追踪标识")
level := fs.String("level", "", "级别(精确匹配)")
service := fs.String("service", "", "服务名")
q := fs.String("q", "", "关键词")
token := fs.String("token", "", "令牌")
if err := fs.Parse(args); err != nil {
return 2
}
u, err := url.Parse(strings.TrimRight(*addr, "/") + "/v1/logs")
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
params := u.Query()
set := func(k, v string) {
if v != "" {
params.Set(k, v)
}
}
set("trace_id", *traceID)
set("level", *level)
set("service", *service)
set("q", *q)
u.RawQuery = params.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
if *token != "" {
req.Header.Set("Authorization", "Bearer "+*token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusOK {
fmt.Fprintf(stderr, "query %s: %s\n", res.Status, body)
return 1
}
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
stdout.Write(body)
return 0
}
fmt.Fprintln(stdout, pretty.String())
return 0
}
func cmdTail(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("tail", flag.ContinueOnError)
fs.SetOutput(stderr)
addr := fs.String("addr", "http://127.0.0.1:8092", "采集器地址")
traceID := fs.String("trace", "", "只打印该追踪标识")
token := fs.String("token", "", "令牌")
if err := fs.Parse(args); err != nil {
return 2
}
u := strings.TrimRight(*addr, "/") + "/v1/logs/tail"
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
if *token != "" {
req.Header.Set("Authorization", "Bearer "+*token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
fmt.Fprintf(stderr, "tail %s: %s\n", res.Status, body)
return 1
}
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
payload := strings.TrimPrefix(line, "data: ")
if *traceID != "" && !strings.Contains(payload, *traceID) {
continue
}
fmt.Fprintln(stdout, payload)
}
if err := sc.Err(); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
return 0
}