Phase 0 (项目骨架):
- main.go 改为最小骨架 (config + logging + gin + /healthz + 优雅退出)
- internal/{config,logging,storage,cluster,...}/ 目录占位
Phase 1 (配置 + 日志):
- internal/config: env 解析 + 必填校验 + token 隐藏的 String()
- internal/logging: slog multi-handler 双输出 (终端 text + 主文件 JSON)
+ StartToolCall per-tool 独立文件 (0600), tools 目录 0700
Phase 2 续 (存储层):
- internal/cluster: 16 字段 Cluster struct (AuthPassword json:"-")
- internal/storage: modernc.org/sqlite 接入, WAL 模式, schema 自动迁移
- ClusterRepo CRUD: Create/Get/List/Update/Delete + ErrNotFound
+ AuthPassword 空字符串 = 保留旧密码 (核心约定)
- 7 个单元测试全绿 (:memory: DB)
- created_at/updated_at 改纳秒精度, 消除 List 测试的 sleep 特殊 case
Co-Authored-By: Claude <noreply@anthropic.com>
136 lines
3.2 KiB
Go
136 lines
3.2 KiB
Go
package logging
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Config configures the application logger.
|
|
type Config struct {
|
|
Level string // "debug"|"info"|"warn"|"error"
|
|
Dir string // e.g. "./data/logs"
|
|
Format string // "text"|"json" — controls terminal output; files are always JSON
|
|
LogToStd bool // true: enable terminal output
|
|
}
|
|
|
|
// ToolCallLogger records the lifecycle of a single tool call.
|
|
type ToolCallLogger interface {
|
|
WithResult(result any)
|
|
WithError(err error)
|
|
End()
|
|
}
|
|
|
|
// logDir is shared with StartToolCall so the per-tool-call logger knows where
|
|
// to create files. It is set by New and read by StartToolCall.
|
|
var (
|
|
logDir string
|
|
logDirMu sync.RWMutex
|
|
)
|
|
|
|
func parseLevel(s string) slog.Level {
|
|
switch strings.ToLower(s) {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// multiHandler fans out records to multiple underlying handlers.
|
|
type multiHandler struct {
|
|
handlers []slog.Handler
|
|
}
|
|
|
|
func (m *multiHandler) Enabled(ctx context.Context, l slog.Level) bool {
|
|
for _, h := range m.handlers {
|
|
if h.Enabled(ctx, l) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
|
|
var errs []error
|
|
for _, h := range m.handlers {
|
|
if h.Enabled(ctx, r.Level) {
|
|
if err := h.Handle(ctx, r); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
handlers := make([]slog.Handler, len(m.handlers))
|
|
for i, h := range m.handlers {
|
|
handlers[i] = h.WithAttrs(attrs)
|
|
}
|
|
return &multiHandler{handlers: handlers}
|
|
}
|
|
|
|
func (m *multiHandler) WithGroup(name string) slog.Handler {
|
|
handlers := make([]slog.Handler, len(m.handlers))
|
|
for i, h := range m.handlers {
|
|
handlers[i] = h.WithGroup(name)
|
|
}
|
|
return &multiHandler{handlers: handlers}
|
|
}
|
|
|
|
// New creates the main application logger and a shutdown closure.
|
|
func New(cfg Config) (*slog.Logger, func(), error) {
|
|
if err := os.MkdirAll(cfg.Dir, 0o755); err != nil {
|
|
return nil, nil, fmt.Errorf("logging: create log dir: %w", err)
|
|
}
|
|
|
|
toolsDir := filepath.Join(cfg.Dir, "tools")
|
|
if err := os.MkdirAll(toolsDir, 0o700); err != nil {
|
|
return nil, nil, fmt.Errorf("logging: create tools log dir: %w", err)
|
|
}
|
|
|
|
logDirMu.Lock()
|
|
logDir = cfg.Dir
|
|
logDirMu.Unlock()
|
|
|
|
level := parseLevel(cfg.Level)
|
|
opts := &slog.HandlerOptions{Level: level}
|
|
|
|
mainPath := filepath.Join(cfg.Dir, "spark-mcp.log")
|
|
f, err := os.OpenFile(mainPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("logging: open main log file: %w", err)
|
|
}
|
|
|
|
fileHandler := slog.NewJSONHandler(f, opts)
|
|
|
|
var handler slog.Handler = fileHandler
|
|
if cfg.LogToStd {
|
|
var stdHandler slog.Handler
|
|
switch strings.ToLower(cfg.Format) {
|
|
case "json":
|
|
stdHandler = slog.NewJSONHandler(os.Stdout, opts)
|
|
default:
|
|
stdHandler = slog.NewTextHandler(os.Stdout, opts)
|
|
}
|
|
handler = &multiHandler{handlers: []slog.Handler{fileHandler, stdHandler}}
|
|
}
|
|
|
|
shutdown := func() {
|
|
_ = f.Close()
|
|
}
|
|
|
|
logger := slog.New(handler).With("component", "main")
|
|
return logger, shutdown, nil
|
|
}
|