Files
sundynix-agentix/sundynix-mcp-go/cmd/server/main.go
T
Blizzard 218fba559c feat(observability): slog 日志带 trace_id,与链路双向互跳
新增 sundynix-shared/otelx/slog.go:traceHandler 包装 slog.Handler,凡 ctx 有
活跃 span 的 slog.InfoContext(ctx,...) 自动注入 trace_id/span_id;SetupSlog(服务名)
装全局 JSON slog(service 标签 + LOG_LEVEL 控级)并设默认;TraceID(ctx) 辅助取 hex。

- 三个服务 main 启动调 otelx.SetupSlog。
- gateway 访问日志 Observe() 改用 slog.InfoContext(c.Request.Context(),...)(删旧
  accessLogger)→ 每条 HTTP 日志带 trace_id。
- dispatcher orchestrator.Handle 的 received/done/error 改 ctx-aware slog → 任务
  执行日志带 trace_id。
- otelx 单测 5 例(注入/无 span 不注入/With() 后仍生效/级别解析)。
- production_readiness.md 1.1:可观测性三件套(metrics+logs带trace_id+traces)闭环。

验证:dispatcher「task done」日志 trace_id 拿去 Jaeger /api/traces/<id> 命中同一条
11 span/3 服务链路;gateway 访问日志亦带同一 trace_id。四模块 build+vet+test 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:41:33 +08:00

107 lines
4.1 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.
// Command server 启动 sundynix-mcp-go —— 第 5 层 Go I/O 型 MCP 工具微服务。
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
sharedbus "github.com/sundynix/sundynix-shared/bus"
"github.com/sundynix/sundynix-shared/contract"
"github.com/sundynix/sundynix-mcp-go/internal/history"
"github.com/sundynix/sundynix-mcp-go/internal/mcp"
"github.com/sundynix/sundynix-mcp-go/internal/memory"
"github.com/sundynix/sundynix-mcp-go/internal/rag"
"github.com/sundynix/sundynix-mcp-go/internal/search"
"github.com/sundynix/sundynix-shared/otelx"
"github.com/sundynix/sundynix-shared/secrets"
)
func main() {
secrets.MustHaveKeyInProd() // 生产须配 SUNDYNIX_SECRET_KEY 以解密下发的 api_key 密文
otelx.SetupSlog("sundynix-mcp-go") // 结构化日志 + 链路感知(工具日志带 trace_id)
// 链路追踪:工具服务端 span 续上 dispatcher 的 tracetool.call → tool.serve)。
shutdownTrace, _ := otelx.Init(context.Background(), "sundynix-mcp-go")
defer func() { _ = shutdownTrace(context.Background()) }()
natsURL := envOr("NATS_URL", "nats://localhost:4222")
pgDSN := envOr("POSTGRES_DSN", "postgres://sundynix:sundynix@localhost:5432/sundynix?sslmode=disable")
redisAddr := envOr("REDIS_ADDR", "localhost:6379")
milvusAddr := envOr("MILVUS_ADDR", "localhost:19530")
embBase := envOr("EMBED_BASE_URL", "") // OpenAI 兼容 embeddings 端点(空=向量检索降级)
embKey := envOr("EMBED_API_KEY", "")
embModel := envOr("EMBED_MODEL", "")
rerankBase := envOr("RERANK_BASE_URL", "") // DashScope 文本重排端点(空=不启用 rerank)
rerankKey := envOr("RERANK_API_KEY", "")
rerankModel := envOr("RERANK_MODEL", "")
neo4jURI := envOr("NEO4J_URI", "neo4j://localhost:7687") // GraphRAG 图谱(连不上则降级)
neo4jUser := envOr("NEO4J_USER", "neo4j")
neo4jPass := envOr("NEO4J_PASS", "sundynix")
b, err := sharedbus.Connect(natsURL)
if err != nil {
log.Fatalf("[mcp_go] nats connect: %v", err)
}
defer b.Close()
log.Printf("[mcp_go] connected %s", natsURL)
engine := search.NewHybrid() // LLM Wiki 混合检索:Bleve + Milvus + Neo4j
mem := memory.Open(pgDSN) // 偏好记忆:sundynix_user_profile(连不上则降级)
defer mem.Close()
hist := history.Open(redisAddr) // 会话短期历史:Redis(连不上则降级)
defer hist.Close()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// RAG 核心链:embedding + Milvus(向量) + Bleve(全文) + Neo4j(图谱) + 可选 rerank
ragEngine := rag.Open(ctx, rag.Config{
MilvusAddr: milvusAddr,
EmbedBase: embBase, EmbedKey: embKey, EmbedModel: embModel,
RerankBase: rerankBase, RerankKey: rerankKey, RerankModel: rerankModel,
Neo4jURI: neo4jURI, Neo4jUser: neo4jUser, Neo4jPass: neo4jPass,
})
defer ragEngine.Close()
// 配置控制面:取激活 embedding(向量) + chat(图谱抽取) 配置并订阅热更新。
applyEmbed := func(cfg *contract.ModelConfig) {
if cfg != nil {
ragEngine.SetEmbedding(cfg.BaseURL, cfg.APIKey, cfg.Model)
}
}
applyChat := func(cfg *contract.ModelConfig) {
if cfg != nil {
ragEngine.SetChat(cfg.BaseURL, cfg.APIKey, cfg.Model)
}
}
// 先订阅热更新(控制台改配置即生效)。
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindChat, applyChat); err != nil {
log.Printf("[mcp_go] subscribe chat config: %v", err)
}
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindEmbedding, applyEmbed); err != nil {
log.Printf("[mcp_go] subscribe embedding config: %v", err)
}
// 后台重试拉初始配置:容忍 gateway 晚于本服务启动(避免一次性扑空致 RAG 长期降级)。
go b.RequestConfigWithRetry(ctx, contract.ConfigKindEmbedding, applyEmbed)
go b.RequestConfigWithRetry(ctx, contract.ConfigKindChat, applyChat)
gw := mcp.NewGateway(b, engine, mem, hist, ragEngine)
log.Println("[mcp_go] serving MCP over sundynix.tools.go.* (Ctrl-C to quit)")
if err := gw.Serve(ctx); err != nil && err != context.Canceled {
log.Fatalf("[mcp_go] exit: %v", err)
}
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}