Files
Cyrene_v2/internal/services/gateway/observability_logs.go
T
AskaEth 074ffc4ebf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
2026-10-03 22:28:25 +08:00

130 lines
3.6 KiB
Go
Raw 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 gateway
import (
"log/slog"
"net/http"
"strconv"
"strings"
"time"
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// 日志尾部端点的调参。编译期常量(项目规范 §10)。
const (
defaultLogLimit = 200
maxLogLimit = 1000
)
// AdminLogEntry 是 GET /api/v1/admin/observability/logs 中一个条目的
// 线上结构:{time,level,source,msg}。`time` 是
// RFC3339 UTC(与 trace/audit 一致);`level` ∈ debug|info|warn|error;`source`
// 是记录的来源标签,记录未携带时为空。
type AdminLogEntry struct {
Time string `json:"time"`
Level string `json:"level"`
Source string `json:"source"`
Msg string `json:"msg"`
}
// SetLogBuffer 注入 GET /api/v1/admin/observability/logs 背后的
// 进程级日志环形缓冲。组合根在网关存在后调用它
// (logger 先于网关构建);在那之前该
// 端点返回 501。并发调用是安全的。
func (s *Server) SetLogBuffer(buf *logger.RingBuffer) {
if s == nil || s.obs == nil {
return
}
s.obs.logBuf.Store(buf)
}
// logBuffer 返回已注入的日志环形缓冲,未注入时为 nil。
func (s *Server) logBuffer() *logger.RingBuffer {
if s == nil || s.obs == nil {
return nil
}
return s.obs.logBuf.Load()
}
// adminObservabilityLogs 返回最近的后端日志记录,最新
// 优先,可选按最低级别过滤(管理读)。它是面板日志
// 尾部背后的后端:记录来自 logger 自身的环形
// 缓冲,没有任何东西被重新渲染或编造。
func (s *Server) adminObservabilityLogs(w http.ResponseWriter, r *http.Request) {
minLevel, filtered, ok := parseLogLevel(r.URL.Query().Get("level"))
if !ok {
writeError(w, &apperr.AppError{
Code: "invalid_level",
Message: "level 只能为 debug/info/warn/error 或留空",
HTTPCode: http.StatusBadRequest,
})
return
}
buf := s.logBuffer()
if buf == nil {
writeError(w, &apperr.AppError{
Code: "not_implemented",
Message: "日志环形缓冲未注入(组合根未挂载 logger.RingBuffer)",
HTTPCode: http.StatusNotImplemented,
})
return
}
limit := parseLogLimit(r)
entries := buf.Snapshot(0)
out := make([]AdminLogEntry, 0, min(len(entries), limit))
for _, e := range entries {
if filtered && logger.ParseLevel(e.Level) < minLevel {
continue
}
out = append(out, adminLogOf(e))
if len(out) == limit {
break
}
}
writeJSON(w, http.StatusOK, out)
}
// adminLogOf 把捕获的日志条目映射为驼峰式管理线上结构。
func adminLogOf(e logger.Entry) AdminLogEntry {
return AdminLogEntry{
Time: e.Time.UTC().Format(time.RFC3339),
Level: e.Level,
Source: e.Source,
Msg: e.Msg,
}
}
// parseLogLevel 解析 ?level= 过滤器。空值关闭过滤
// (filtered=false);debug/info/warn/error 选中该级别及以上;其他任何
// 取值都以 ok=false 拒绝,使 handler 返回 400。
func parseLogLevel(raw string) (min slog.Level, filtered, ok bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "":
return 0, false, true
case "debug":
return slog.LevelDebug, true, true
case "info":
return slog.LevelInfo, true, true
case "warn":
return slog.LevelWarn, true, true
case "error":
return slog.LevelError, true, true
default:
return 0, false, false
}
}
// parseLogLimit 解析 ?limit= 查询参数,夹取到 [1, maxLogLimit] 并在
// 缺失或非法时选用 defaultLogLimit。
func parseLogLimit(r *http.Request) int {
n, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || n <= 0 {
return defaultLogLimit
}
if n > maxLogLimit {
return maxLogLimit
}
return n
}