diff --git a/cmd/agent/main.go b/cmd/agent/main.go index f67636ed..e91d2de1 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -3,13 +3,14 @@ package main import ( "context" "fmt" - "log" + "log/slog" "os" "strings" "time" "github.com/memohai/memoh/internal/chat" "github.com/memohai/memoh/internal/config" + "github.com/memohai/memoh/internal/logger" ctr "github.com/memohai/memoh/internal/containerd" "github.com/memohai/memoh/internal/db" dbsqlc "github.com/memohai/memoh/internal/db/sqlc" @@ -34,15 +35,20 @@ func main() { cfgPath := os.Getenv("CONFIG_PATH") cfg, err := config.Load(cfgPath) if err != nil { - log.Fatalf("load config: %v", err) + fmt.Fprintf(os.Stderr, "load config: %v\n", err) + os.Exit(1) } + logger.Init(cfg.Log.Level, cfg.Log.Format) + if strings.TrimSpace(cfg.Auth.JWTSecret) == "" { - log.Fatalf("jwt secret is required") + logger.Error("jwt secret is required") + os.Exit(1) } jwtExpiresIn, err := time.ParseDuration(cfg.Auth.JWTExpiresIn) if err != nil { - log.Fatalf("invalid jwt expires in: %v", err) + logger.Error("invalid jwt expires in", slog.Any("error", err)) + os.Exit(1) } addr := cfg.Server.Addr @@ -57,30 +63,33 @@ func main() { factory := ctr.DefaultClientFactory{SocketPath: socketPath} client, err := factory.New(ctx) if err != nil { - log.Fatalf("connect containerd: %v", err) + logger.Error("connect containerd", slog.Any("error", err)) + os.Exit(1) } defer client.Close() - service := ctr.NewDefaultService(client, cfg.Containerd.Namespace) - manager := mcp.NewManager(service, cfg.MCP) + service := ctr.NewDefaultService(logger.L, client, cfg.Containerd.Namespace) + manager := mcp.NewManager(logger.L, service, cfg.MCP) - pingHandler := handlers.NewPingHandler() - containerdHandler := handlers.NewContainerdHandler(service, cfg.MCP, cfg.Containerd.Namespace) + pingHandler := handlers.NewPingHandler(logger.L) + containerdHandler := handlers.NewContainerdHandler(logger.L, service, cfg.MCP, cfg.Containerd.Namespace) conn, err := db.Open(ctx, cfg.Postgres) if err != nil { - log.Fatalf("db connect: %v", err) + logger.Error("db connect", slog.Any("error", err)) + os.Exit(1) } defer conn.Close() manager.WithDB(conn) queries := dbsqlc.New(conn) - modelsService := models.NewService(queries) + modelsService := models.NewService(logger.L, queries) - if err := ensureAdminUser(ctx, queries, cfg); err != nil { - log.Fatalf("ensure admin user: %v", err) + if err := ensureAdminUser(ctx, logger.L, queries, cfg); err != nil { + logger.Error("ensure admin user", slog.Any("error", err)) + os.Exit(1) } - authHandler := handlers.NewAuthHandler(conn, cfg.Auth.JWTSecret, jwtExpiresIn) + authHandler := handlers.NewAuthHandler(logger.L, conn, cfg.Auth.JWTSecret, jwtExpiresIn) // Initialize chat resolver after memory service is configured. var chatResolver *chat.Resolver @@ -90,27 +99,29 @@ func main() { modelsService: modelsService, queries: queries, timeout: 30 * time.Second, + logger: logger.L, } - resolver := embeddings.NewResolver(modelsService, queries, 10*time.Second) + resolver := embeddings.NewResolver(logger.L, modelsService, queries, 10*time.Second) vectors, textModel, multimodalModel, hasModels, err := embeddings.CollectEmbeddingVectors(ctx, modelsService) if err != nil { - log.Fatalf("embedding models: %v", err) + logger.Error("embedding models", slog.Any("error", err)) + os.Exit(1) } var memoryService *memory.Service var memoryHandler *handlers.MemoryHandler if !hasModels { - log.Println("WARNING: No embedding models configured. Memory service will not be available.") - log.Println("You can add embedding models via the /models API endpoint.") - memoryHandler = handlers.NewMemoryHandler(nil) + logger.Warn("No embedding models configured. Memory service will not be available.") + logger.Warn("You can add embedding models via the /models API endpoint.") + memoryHandler = handlers.NewMemoryHandler(logger.L, nil) } else { if textModel.ModelID == "" { - log.Println("WARNING: No text embedding model configured. Text embedding features will be limited.") + logger.Warn("No text embedding model configured. Text embedding features will be limited.") } if multimodalModel.ModelID == "" { - log.Println("WARNING: No multimodal embedding model configured. Multimodal embedding features will be limited.") + logger.Warn("No multimodal embedding model configured. Multimodal embedding features will be limited.") } var textEmbedder embeddings.Embedder @@ -125,6 +136,7 @@ func main() { if len(vectors) > 0 { store, err = memory.NewQdrantStoreWithVectors( + logger.L, cfg.Qdrant.BaseURL, cfg.Qdrant.APIKey, cfg.Qdrant.Collection, @@ -132,10 +144,12 @@ func main() { time.Duration(cfg.Qdrant.TimeoutSeconds)*time.Second, ) if err != nil { - log.Fatalf("qdrant named vectors init: %v", err) + logger.Error("qdrant named vectors init", slog.Any("error", err)) + os.Exit(1) } } else { store, err = memory.NewQdrantStore( + logger.L, cfg.Qdrant.BaseURL, cfg.Qdrant.APIKey, cfg.Qdrant.Collection, @@ -143,42 +157,45 @@ func main() { time.Duration(cfg.Qdrant.TimeoutSeconds)*time.Second, ) if err != nil { - log.Fatalf("qdrant init: %v", err) + logger.Error("qdrant init", slog.Any("error", err)) + os.Exit(1) } } } - memoryService = memory.NewService(llmClient, textEmbedder, store, resolver, textModel.ModelID, multimodalModel.ModelID) - memoryHandler = handlers.NewMemoryHandler(memoryService) + memoryService = memory.NewService(logger.L, llmClient, textEmbedder, store, resolver, textModel.ModelID, multimodalModel.ModelID) + memoryHandler = handlers.NewMemoryHandler(logger.L, memoryService) } - chatResolver = chat.NewResolver(modelsService, queries, memoryService, cfg.AgentGateway.BaseURL(), 30*time.Second) - embeddingsHandler := handlers.NewEmbeddingsHandler(modelsService, queries) - swaggerHandler := handlers.NewSwaggerHandler() - chatHandler := handlers.NewChatHandler(chatResolver) + chatResolver = chat.NewResolver(logger.L, modelsService, queries, memoryService, cfg.AgentGateway.BaseURL(), 30*time.Second) + embeddingsHandler := handlers.NewEmbeddingsHandler(logger.L, modelsService, queries) + swaggerHandler := handlers.NewSwaggerHandler(logger.L) + chatHandler := handlers.NewChatHandler(logger.L, chatResolver) // Initialize providers and models handlers - providersService := providers.NewService(queries) - providersHandler := handlers.NewProvidersHandler(providersService) - modelsHandler := handlers.NewModelsHandler(modelsService) - settingsService := settings.NewService(queries) - settingsHandler := handlers.NewSettingsHandler(settingsService) - historyService := history.NewService(queries) - historyHandler := handlers.NewHistoryHandler(historyService) - scheduleService := schedule.NewService(queries, chatResolver, cfg.Auth.JWTSecret) + providersService := providers.NewService(logger.L, queries) + providersHandler := handlers.NewProvidersHandler(logger.L, providersService) + modelsHandler := handlers.NewModelsHandler(logger.L, modelsService) + settingsService := settings.NewService(logger.L, queries) + settingsHandler := handlers.NewSettingsHandler(logger.L, settingsService) + historyService := history.NewService(logger.L, queries) + historyHandler := handlers.NewHistoryHandler(logger.L, historyService) + scheduleService := schedule.NewService(logger.L, queries, chatResolver, cfg.Auth.JWTSecret) if err := scheduleService.Bootstrap(ctx); err != nil { - log.Fatalf("schedule bootstrap: %v", err) + logger.Error("schedule bootstrap", slog.Any("error", err)) + os.Exit(1) } - scheduleHandler := handlers.NewScheduleHandler(scheduleService) - subagentService := subagent.NewService(queries) - subagentHandler := handlers.NewSubagentHandler(subagentService) - srv := server.NewServer(addr, cfg.Auth.JWTSecret, pingHandler, authHandler, memoryHandler, embeddingsHandler, chatHandler, swaggerHandler, providersHandler, modelsHandler, settingsHandler, historyHandler, scheduleHandler, subagentHandler, containerdHandler) + scheduleHandler := handlers.NewScheduleHandler(logger.L, scheduleService) + subagentService := subagent.NewService(logger.L, queries) + subagentHandler := handlers.NewSubagentHandler(logger.L, subagentService) + srv := server.NewServer(logger.L, addr, cfg.Auth.JWTSecret, pingHandler, authHandler, memoryHandler, embeddingsHandler, chatHandler, swaggerHandler, providersHandler, modelsHandler, settingsHandler, historyHandler, scheduleHandler, subagentHandler, containerdHandler) if err := srv.Start(); err != nil { - log.Fatalf("server failed: %v", err) + logger.Error("server failed", slog.Any("error", err)) + os.Exit(1) } } -func ensureAdminUser(ctx context.Context, queries *dbsqlc.Queries, cfg config.Config) error { +func ensureAdminUser(ctx context.Context, log *slog.Logger, queries *dbsqlc.Queries, cfg config.Config) error { if queries == nil { return fmt.Errorf("db queries not configured") } @@ -197,7 +214,7 @@ func ensureAdminUser(ctx context.Context, queries *dbsqlc.Queries, cfg config.Co return fmt.Errorf("admin username/password required in config.toml") } if password == "change-your-password-here" { - log.Printf("WARNING: admin password uses default placeholder; please update config.toml") + log.Warn("admin password uses default placeholder; please update config.toml") } hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -225,7 +242,7 @@ func ensureAdminUser(ctx context.Context, queries *dbsqlc.Queries, cfg config.Co if err != nil { return err } - log.Printf("Admin user created: %s", username) + log.Info("Admin user created", slog.String("username", username)) return nil } @@ -233,6 +250,7 @@ type lazyLLMClient struct { modelsService *models.Service queries *dbsqlc.Queries timeout time.Duration + logger *slog.Logger } func (c *lazyLLMClient) Extract(ctx context.Context, req memory.ExtractRequest) (memory.ExtractResponse, error) { @@ -263,5 +281,5 @@ func (c *lazyLLMClient) resolve(ctx context.Context) (memory.LLM, error) { if clientType != "openai" && clientType != "openai-compat" { return nil, fmt.Errorf("memory provider client type not supported: %s", memoryProvider.ClientType) } - return memory.NewLLMClient(memoryProvider.BaseUrl, memoryProvider.ApiKey, memoryModel.ModelID, c.timeout), nil + return memory.NewLLMClient(c.logger, memoryProvider.BaseUrl, memoryProvider.ApiKey, memoryModel.ModelID, c.timeout), nil } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 79844741..746f23bb 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -8,7 +8,7 @@ import ( "flag" "fmt" "io" - "log" + "log/slog" "net/http" "os" "strings" @@ -16,6 +16,7 @@ import ( "github.com/memohai/memoh/internal/chat" "github.com/memohai/memoh/internal/config" + "github.com/memohai/memoh/internal/logger" ) type cliOptions struct { @@ -33,13 +34,18 @@ func main() { cfg, err := config.Load(opts.configPath) if err != nil { - log.Fatalf("load config: %v", err) + fmt.Fprintf(os.Stderr, "load config: %v\n", err) + os.Exit(1) } + + logger.Init(cfg.Log.Level, cfg.Log.Format) + if strings.TrimSpace(opts.apiBaseURL) == "" { opts.apiBaseURL = defaultAPIBaseURL(cfg.Server.Addr) } if strings.TrimSpace(opts.apiBaseURL) == "" { - log.Fatalf("api url is required") + logger.Error("api url is required") + os.Exit(1) } opts.apiBaseURL = normalizeBaseURL(opts.apiBaseURL) @@ -48,7 +54,8 @@ func main() { if jwtToken == "" { username, password, err := resolveLoginCredentials(opts, cfg) if err != nil { - log.Fatalf("resolve login: %v", err) + logger.Error("resolve login", slog.Any("error", err)) + os.Exit(1) } loginCtx := ctx if opts.timeout > 0 { @@ -58,19 +65,22 @@ func main() { } jwtToken, err = resolveJWTToken(loginCtx, client, opts.apiBaseURL, username, password) if err != nil { - log.Fatalf("resolve jwt: %v", err) + logger.Error("resolve jwt", slog.Any("error", err)) + os.Exit(1) } } query := strings.TrimSpace(strings.Join(flag.Args(), " ")) if query != "" { if err := sendChat(ctx, client, opts.apiBaseURL, jwtToken, query); err != nil { - log.Fatalf("chat failed: %v", err) + logger.Error("chat failed", slog.Any("error", err)) + os.Exit(1) } return } if err := runInteractive(ctx, client, opts.apiBaseURL, jwtToken); err != nil { - log.Fatalf("chat failed: %v", err) + logger.Error("chat failed", slog.Any("error", err)) + os.Exit(1) } } diff --git a/cmd/mcp/main.go b/cmd/mcp/main.go index 2a8f3f20..91fa74f1 100644 --- a/cmd/mcp/main.go +++ b/cmd/mcp/main.go @@ -2,8 +2,10 @@ package main import ( "context" - "log" + "log/slog" + "os" + "github.com/memohai/memoh/internal/logger" "github.com/memohai/memoh/internal/mcp" gomcp "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -17,12 +19,24 @@ func main() { if version == "unknown" { version = "v0.0.0-dev+" + commitHash } + + logLevel := os.Getenv("LOG_LEVEL") + if logLevel == "" { + logLevel = "info" + } + logFormat := os.Getenv("LOG_FORMAT") + if logFormat == "" { + logFormat = "text" + } + logger.Init(logLevel, logFormat) + server := gomcp.NewServer( &gomcp.Implementation{Name: "memoh-mcp", Version: version}, nil, ) mcp.RegisterTools(server) if err := server.Run(context.Background(), &gomcp.StdioTransport{}); err != nil { - log.Fatal(err) + logger.Error("mcp server failed", slog.Any("error", err)) + os.Exit(1) } } diff --git a/config.toml.example b/config.toml.example index 8b6754b0..01e8019f 100644 --- a/config.toml.example +++ b/config.toml.example @@ -1,4 +1,8 @@ ## Service configuration +[log] +level = "info" +format = "text" + [server] # HTTP listen address addr = ":8080" diff --git a/internal/chat/resolver.go b/internal/chat/resolver.go index 0bd90077..e1030607 100644 --- a/internal/chat/resolver.go +++ b/internal/chat/resolver.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "strings" "time" @@ -30,11 +31,12 @@ type Resolver struct { memoryService *memory.Service gatewayBaseURL string timeout time.Duration + logger *slog.Logger httpClient *http.Client streamingClient *http.Client } -func NewResolver(modelsService *models.Service, queries *sqlc.Queries, memoryService *memory.Service, gatewayBaseURL string, timeout time.Duration) *Resolver { +func NewResolver(log *slog.Logger, modelsService *models.Service, queries *sqlc.Queries, memoryService *memory.Service, gatewayBaseURL string, timeout time.Duration) *Resolver { if strings.TrimSpace(gatewayBaseURL) == "" { gatewayBaseURL = "http://127.0.0.1:8081" } @@ -48,6 +50,7 @@ func NewResolver(modelsService *models.Service, queries *sqlc.Queries, memorySer memoryService: memoryService, gatewayBaseURL: gatewayBaseURL, timeout: timeout, + logger: log.With(slog.String("service", "chat")), httpClient: &http.Client{ Timeout: timeout, }, diff --git a/internal/config/config.go b/internal/config/config.go index 557c2784..3f4d39d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,7 @@ const ( ) type Config struct { + Log LogConfig `toml:"log"` Server ServerConfig `toml:"server"` Admin AdminConfig `toml:"admin"` Auth AuthConfig `toml:"auth"` @@ -36,6 +37,11 @@ type Config struct { AgentGateway AgentGatewayConfig `toml:"agent_gateway"` } +type LogConfig struct { + Level string `toml:"level"` + Format string `toml:"format"` +} + type ServerConfig struct { Addr string `toml:"addr"` } @@ -98,6 +104,10 @@ func (c AgentGatewayConfig) BaseURL() string { func Load(path string) (Config, error) { cfg := Config{ + Log: LogConfig{ + Level: "info", + Format: "text", + }, Server: ServerConfig{ Addr: DefaultHTTPAddr, }, diff --git a/internal/containerd/service.go b/internal/containerd/service.go index d90dd3e4..6ab0d3a3 100644 --- a/internal/containerd/service.go +++ b/internal/containerd/service.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log/slog" "os" "path/filepath" "runtime" @@ -144,15 +145,17 @@ type Service interface { type DefaultService struct { client *containerd.Client namespace string + logger *slog.Logger } -func NewDefaultService(client *containerd.Client, namespace string) *DefaultService { +func NewDefaultService(log *slog.Logger, client *containerd.Client, namespace string) *DefaultService { if namespace == "" { namespace = DefaultNamespace } return &DefaultService{ client: client, namespace: namespace, + logger: log.With(slog.String("service", "containerd")), } } diff --git a/internal/embeddings/dashscope.go b/internal/embeddings/dashscope.go index 939e8822..c15fc797 100644 --- a/internal/embeddings/dashscope.go +++ b/internal/embeddings/dashscope.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "strings" "time" @@ -20,6 +21,7 @@ type DashScopeEmbedder struct { apiKey string baseURL string model string + logger *slog.Logger http *http.Client } @@ -53,7 +55,7 @@ type dashScopeResponse struct { Message string `json:"message"` } -func NewDashScopeEmbedder(apiKey, baseURL, model string, timeout time.Duration) *DashScopeEmbedder { +func NewDashScopeEmbedder(log *slog.Logger, apiKey, baseURL, model string, timeout time.Duration) *DashScopeEmbedder { if baseURL == "" { baseURL = DefaultDashScopeBaseURL } @@ -64,6 +66,7 @@ func NewDashScopeEmbedder(apiKey, baseURL, model string, timeout time.Duration) apiKey: apiKey, baseURL: strings.TrimRight(baseURL, "/"), model: model, + logger: log.With(slog.String("embedder", "dashscope")), http: &http.Client{ Timeout: timeout, }, diff --git a/internal/embeddings/embeddings.go b/internal/embeddings/embeddings.go index 808c9dfb..e907c5b4 100644 --- a/internal/embeddings/embeddings.go +++ b/internal/embeddings/embeddings.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "strings" "time" @@ -21,6 +22,7 @@ type OpenAIEmbedder struct { baseURL string model string dims int + logger *slog.Logger http *http.Client } @@ -35,7 +37,7 @@ type openAIEmbeddingResponse struct { } `json:"data"` } -func NewOpenAIEmbedder(apiKey, baseURL, model string, dims int, timeout time.Duration) *OpenAIEmbedder { +func NewOpenAIEmbedder(log *slog.Logger, apiKey, baseURL, model string, dims int, timeout time.Duration) *OpenAIEmbedder { if baseURL == "" { baseURL = "https://api.openai.com" } @@ -53,6 +55,7 @@ func NewOpenAIEmbedder(apiKey, baseURL, model string, dims int, timeout time.Dur baseURL: strings.TrimRight(baseURL, "/"), model: model, dims: dims, + logger: log.With(slog.String("embedder", "openai")), http: &http.Client{ Timeout: timeout, }, diff --git a/internal/embeddings/resolver.go b/internal/embeddings/resolver.go index 44373afb..c8becb7f 100644 --- a/internal/embeddings/resolver.go +++ b/internal/embeddings/resolver.go @@ -3,6 +3,7 @@ package embeddings import ( "context" "errors" + "log/slog" "strings" "time" @@ -55,13 +56,15 @@ type Resolver struct { modelsService *models.Service queries *sqlc.Queries timeout time.Duration + logger *slog.Logger } -func NewResolver(modelsService *models.Service, queries *sqlc.Queries, timeout time.Duration) *Resolver { +func NewResolver(log *slog.Logger, modelsService *models.Service, queries *sqlc.Queries, timeout time.Duration) *Resolver { return &Resolver{ modelsService: modelsService, queries: queries, timeout: timeout, + logger: log.With(slog.String("service", "embeddings")), } } @@ -127,7 +130,7 @@ func (r *Resolver) Embed(ctx context.Context, req Request) (Result, error) { if strings.TrimSpace(provider.ApiKey) == "" { return Result{}, errors.New("openai api key is required") } - embedder := NewOpenAIEmbedder(provider.ApiKey, provider.BaseUrl, req.Model, req.Dimensions, timeout) + embedder := NewOpenAIEmbedder(r.logger, provider.ApiKey, provider.BaseUrl, req.Model, req.Dimensions, timeout) vector, err := embedder.Embed(ctx, req.Input.Text) if err != nil { return Result{}, err @@ -144,7 +147,7 @@ func (r *Resolver) Embed(ctx context.Context, req Request) (Result, error) { if strings.TrimSpace(provider.ApiKey) == "" { return Result{}, errors.New("dashscope api key is required") } - dashscope := NewDashScopeEmbedder(provider.ApiKey, provider.BaseUrl, req.Model, timeout) + dashscope := NewDashScopeEmbedder(r.logger, provider.ApiKey, provider.BaseUrl, req.Model, timeout) vector, usage, err := dashscope.Embed(ctx, req.Input.Text, req.Input.ImageURL, req.Input.VideoURL) if err != nil { return Result{}, err diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index de389181..8481615c 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -3,6 +3,7 @@ package handlers import ( "context" "fmt" + "log/slog" "net/http" "strings" "time" @@ -22,6 +23,7 @@ type AuthHandler struct { db *pgxpool.Pool jwtSecret string expiresIn time.Duration + logger *slog.Logger } type LoginRequest struct { @@ -39,11 +41,12 @@ type LoginResponse struct { Username string `json:"username"` } -func NewAuthHandler(db *pgxpool.Pool, jwtSecret string, expiresIn time.Duration) *AuthHandler { +func NewAuthHandler(log *slog.Logger, db *pgxpool.Pool, jwtSecret string, expiresIn time.Duration) *AuthHandler { return &AuthHandler{ db: db, jwtSecret: jwtSecret, expiresIn: expiresIn, + logger: log.With(slog.String("handler", "auth")), } } diff --git a/internal/handlers/chat.go b/internal/handlers/chat.go index 9d097938..0dad3bc0 100644 --- a/internal/handlers/chat.go +++ b/internal/handlers/chat.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/json" "fmt" + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -15,10 +16,14 @@ import ( type ChatHandler struct { resolver *chat.Resolver + logger *slog.Logger } -func NewChatHandler(resolver *chat.Resolver) *ChatHandler { - return &ChatHandler{resolver: resolver} +func NewChatHandler(log *slog.Logger, resolver *chat.Resolver) *ChatHandler { + return &ChatHandler{ + resolver: resolver, + logger: log.With(slog.String("handler", "chat")), + } } func (h *ChatHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/containerd.go b/internal/handlers/containerd.go index 070d9706..5d309606 100644 --- a/internal/handlers/containerd.go +++ b/internal/handlers/containerd.go @@ -2,7 +2,7 @@ package handlers import ( "context" - "log" + "log/slog" "net/http" "os" "path/filepath" @@ -28,6 +28,7 @@ type ContainerdHandler struct { service ctr.Service cfg config.MCPConfig namespace string + logger *slog.Logger mcpMu sync.Mutex mcpSess map[string]*mcpSession } @@ -85,11 +86,12 @@ type ListSnapshotsResponse struct { Snapshots []SnapshotInfo `json:"snapshots"` } -func NewContainerdHandler(service ctr.Service, cfg config.MCPConfig, namespace string) *ContainerdHandler { +func NewContainerdHandler(log *slog.Logger, service ctr.Service, cfg config.MCPConfig, namespace string) *ContainerdHandler { return &ContainerdHandler{ service: service, cfg: cfg, namespace: namespace, + logger: log.With(slog.String("handler", "containerd")), mcpSess: make(map[string]*mcpSession), } } @@ -196,7 +198,10 @@ func (h *ContainerdHandler) CreateContainer(c echo.Context) error { }); err == nil { started = true } else { - log.Printf("mcp container start failed: id=%s err=%v", req.ContainerID, err) + h.logger.Error("mcp container start failed", + slog.String("container_id", req.ContainerID), + slog.Any("error", err), + ) } return c.JSON(http.StatusOK, CreateContainerResponse{ diff --git a/internal/handlers/embeddings.go b/internal/handlers/embeddings.go index 6ab5f2fa..e74a9fc1 100644 --- a/internal/handlers/embeddings.go +++ b/internal/handlers/embeddings.go @@ -1,6 +1,7 @@ package handlers import ( + "log/slog" "net/http" "strings" "time" @@ -16,6 +17,7 @@ const DefaultEmbeddingTimeout = 10 * time.Second type EmbeddingsHandler struct { resolver *embeddings.Resolver + logger *slog.Logger } type EmbeddingsRequest struct { @@ -48,9 +50,10 @@ type EmbeddingsUsage struct { VideoTokens int `json:"video_tokens,omitempty"` } -func NewEmbeddingsHandler(modelsService *models.Service, queries *sqlc.Queries) *EmbeddingsHandler { +func NewEmbeddingsHandler(log *slog.Logger, modelsService *models.Service, queries *sqlc.Queries) *EmbeddingsHandler { return &EmbeddingsHandler{ - resolver: embeddings.NewResolver(modelsService, queries, DefaultEmbeddingTimeout), + resolver: embeddings.NewResolver(log, modelsService, queries, DefaultEmbeddingTimeout), + logger: log.With(slog.String("handler", "embeddings")), } } diff --git a/internal/handlers/history.go b/internal/handlers/history.go index 635be0ff..bcb8e585 100644 --- a/internal/handlers/history.go +++ b/internal/handlers/history.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -13,10 +14,14 @@ import ( type HistoryHandler struct { service *history.Service + logger *slog.Logger } -func NewHistoryHandler(service *history.Service) *HistoryHandler { - return &HistoryHandler{service: service} +func NewHistoryHandler(log *slog.Logger, service *history.Service) *HistoryHandler { + return &HistoryHandler{ + service: service, + logger: log.With(slog.String("handler", "history")), + } } func (h *HistoryHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/memory.go b/internal/handlers/memory.go index 0eac6ad5..001cb59f 100644 --- a/internal/handlers/memory.go +++ b/internal/handlers/memory.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -13,10 +14,14 @@ import ( type MemoryHandler struct { service *memory.Service + logger *slog.Logger } -func NewMemoryHandler(service *memory.Service) *MemoryHandler { - return &MemoryHandler{service: service} +func NewMemoryHandler(log *slog.Logger, service *memory.Service) *MemoryHandler { + return &MemoryHandler{ + service: service, + logger: log.With(slog.String("handler", "memory")), + } } func (h *MemoryHandler) Register(e *echo.Echo) { @@ -51,7 +56,7 @@ func (h *MemoryHandler) EmbedUpsert(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -86,7 +91,7 @@ func (h *MemoryHandler) Add(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -121,7 +126,7 @@ func (h *MemoryHandler) Search(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -156,7 +161,7 @@ func (h *MemoryHandler) Update(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -196,7 +201,7 @@ func (h *MemoryHandler) Get(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -233,7 +238,7 @@ func (h *MemoryHandler) GetAll(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -274,7 +279,7 @@ func (h *MemoryHandler) Delete(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err @@ -313,7 +318,7 @@ func (h *MemoryHandler) DeleteAll(c echo.Context) error { if err := h.checkService(); err != nil { return err } - + userID, err := h.requireUserID(c) if err != nil { return err diff --git a/internal/handlers/models.go b/internal/handlers/models.go index ad2ec4f8..9fafbca6 100644 --- a/internal/handlers/models.go +++ b/internal/handlers/models.go @@ -1,6 +1,7 @@ package handlers import ( + "log/slog" "net/http" "net/url" @@ -11,10 +12,14 @@ import ( type ModelsHandler struct { service *models.Service + logger *slog.Logger } -func NewModelsHandler(service *models.Service) *ModelsHandler { - return &ModelsHandler{service: service} +func NewModelsHandler(log *slog.Logger, service *models.Service) *ModelsHandler { + return &ModelsHandler{ + service: service, + logger: log.With(slog.String("handler", "models")), + } } func (h *ModelsHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/ping.go b/internal/handlers/ping.go index b33fb2ed..49fc7f9a 100644 --- a/internal/handlers/ping.go +++ b/internal/handlers/ping.go @@ -1,15 +1,18 @@ package handlers import ( + "log/slog" "net/http" "github.com/labstack/echo/v4" ) -type PingHandler struct{} +type PingHandler struct { + logger *slog.Logger +} -func NewPingHandler() *PingHandler { - return &PingHandler{} +func NewPingHandler(log *slog.Logger) *PingHandler { + return &PingHandler{logger: log.With(slog.String("handler", "ping"))} } func (h *PingHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/providers.go b/internal/handlers/providers.go index 58a76973..1e78a7d7 100644 --- a/internal/handlers/providers.go +++ b/internal/handlers/providers.go @@ -1,6 +1,7 @@ package handlers import ( + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -10,10 +11,14 @@ import ( type ProvidersHandler struct { service *providers.Service + logger *slog.Logger } -func NewProvidersHandler(service *providers.Service) *ProvidersHandler { - return &ProvidersHandler{service: service} +func NewProvidersHandler(log *slog.Logger, service *providers.Service) *ProvidersHandler { + return &ProvidersHandler{ + service: service, + logger: log.With(slog.String("handler", "providers")), + } } func (h *ProvidersHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/schedule.go b/internal/handlers/schedule.go index bd7bc0ae..4d6848af 100644 --- a/internal/handlers/schedule.go +++ b/internal/handlers/schedule.go @@ -1,6 +1,7 @@ package handlers import ( + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -12,10 +13,14 @@ import ( type ScheduleHandler struct { service *schedule.Service + logger *slog.Logger } -func NewScheduleHandler(service *schedule.Service) *ScheduleHandler { - return &ScheduleHandler{service: service} +func NewScheduleHandler(log *slog.Logger, service *schedule.Service) *ScheduleHandler { + return &ScheduleHandler{ + service: service, + logger: log.With(slog.String("handler", "schedule")), + } } func (h *ScheduleHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index 93153ff3..8dcde0e8 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -1,6 +1,7 @@ package handlers import ( + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -12,10 +13,14 @@ import ( type SettingsHandler struct { service *settings.Service + logger *slog.Logger } -func NewSettingsHandler(service *settings.Service) *SettingsHandler { - return &SettingsHandler{service: service} +func NewSettingsHandler(log *slog.Logger, service *settings.Service) *SettingsHandler { + return &SettingsHandler{ + service: service, + logger: log.With(slog.String("handler", "settings")), + } } func (h *SettingsHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/subagent.go b/internal/handlers/subagent.go index e4b50d03..7a22c4d9 100644 --- a/internal/handlers/subagent.go +++ b/internal/handlers/subagent.go @@ -1,6 +1,7 @@ package handlers import ( + "log/slog" "net/http" "github.com/labstack/echo/v4" @@ -12,10 +13,14 @@ import ( type SubagentHandler struct { service *subagent.Service + logger *slog.Logger } -func NewSubagentHandler(service *subagent.Service) *SubagentHandler { - return &SubagentHandler{service: service} +func NewSubagentHandler(log *slog.Logger, service *subagent.Service) *SubagentHandler { + return &SubagentHandler{ + service: service, + logger: log.With(slog.String("handler", "subagent")), + } } func (h *SubagentHandler) Register(e *echo.Echo) { diff --git a/internal/handlers/swagger.go b/internal/handlers/swagger.go index da4f31aa..2900ae1a 100644 --- a/internal/handlers/swagger.go +++ b/internal/handlers/swagger.go @@ -4,6 +4,7 @@ package handlers // @version 1.0.0 import ( + "log/slog" "net/http" "os" "sync" @@ -19,10 +20,12 @@ var ( swaggerErr error ) -type SwaggerHandler struct{} +type SwaggerHandler struct { + logger *slog.Logger +} -func NewSwaggerHandler() *SwaggerHandler { - return &SwaggerHandler{} +func NewSwaggerHandler(log *slog.Logger) *SwaggerHandler { + return &SwaggerHandler{logger: log.With(slog.String("handler", "swagger"))} } func (h *SwaggerHandler) Register(e *echo.Echo) { diff --git a/internal/history/service.go b/internal/history/service.go index a9200aad..0b860358 100644 --- a/internal/history/service.go +++ b/internal/history/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "strings" "time" @@ -19,10 +20,14 @@ const defaultListLimit = 50 type Service struct { queries *sqlc.Queries + logger *slog.Logger } -func NewService(queries *sqlc.Queries) *Service { - return &Service{queries: queries} +func NewService(log *slog.Logger, queries *sqlc.Queries) *Service { + return &Service{ + queries: queries, + logger: log.With(slog.String("service", "history")), + } } func (s *Service) Create(ctx context.Context, userID string, req CreateRequest) (Record, error) { diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..fc8f970d --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,66 @@ +package logger + +import ( + "context" + "log/slog" + "os" + "strings" +) + +type ctxKey struct{} + +var ( + L *slog.Logger = slog.Default() + logKey = ctxKey{} +) + +// Init 初始化全局日志 +func Init(level, format string) { + var handler slog.Handler + opts := &slog.HandlerOptions{ + Level: parseLevel(level), + } + + if strings.ToLower(format) == "json" { + handler = slog.NewJSONHandler(os.Stdout, opts) + } else { + handler = slog.NewTextHandler(os.Stdout, opts) + } + + L = slog.New(handler) + slog.SetDefault(L) +} + +// FromContext 从 context 中获取 logger,如果不存在则返回全局 logger +func FromContext(ctx context.Context) *slog.Logger { + if l, ok := ctx.Value(logKey).(*slog.Logger); ok { + return l + } + return L +} + +// WithContext 将 logger 注入 context +func WithContext(ctx context.Context, l *slog.Logger) context.Context { + return context.WithValue(ctx, logKey, l) +} + +func parseLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +// 快捷方法,支持强类型 slog.Attr 或松散的 key-value 对 +func Debug(msg string, args ...any) { L.Debug(msg, args...) } +func Info(msg string, args ...any) { L.Info(msg, args...) } +func Warn(msg string, args ...any) { L.Warn(msg, args...) } +func Error(msg string, args ...any) { L.Error(msg, args...) } diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 00000000..676cd358 --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,55 @@ +package logger + +import ( + "context" + "log/slog" + "testing" +) + +func TestInitAndLogging(t *testing.T) { + // 测试 JSON 格式 + Init("debug", "json") + + if L.Enabled(context.Background(), slog.LevelDebug) != true { + t.Error("expected debug level to be enabled") + } + + // 验证是否能正常输出(不崩溃) + Info("test info message", "key", "value") +} + +func TestContextLogger(t *testing.T) { + Init("info", "text") + + // 创建一个带特定属性的 logger + expectedKey := "request_id" + expectedValue := "12345" + customLogger := L.With(expectedKey, expectedValue) + + ctx := WithContext(context.Background(), customLogger) + extracted := FromContext(ctx) + + // 这里简单验证提取出来的是否是同一个(或者功能一致) + if extracted == nil { + t.Fatal("extracted logger should not be nil") + } +} + +func TestParseLevel(t *testing.T) { + tests := []struct { + input string + expected slog.Level + }{ + {"debug", slog.LevelDebug}, + {"INFO", slog.LevelInfo}, + {"Warn", slog.LevelWarn}, + {"error", slog.LevelError}, + {"unknown", slog.LevelInfo}, + } + + for _, tt := range tests { + if got := parseLevel(tt.input); got != tt.expected { + t.Errorf("parseLevel(%s) = %v, want %v", tt.input, got, tt.expected) + } + } +} diff --git a/internal/mcp/manager.go b/internal/mcp/manager.go index 65317319..f48c1ea6 100644 --- a/internal/mcp/manager.go +++ b/internal/mcp/manager.go @@ -3,6 +3,7 @@ package mcp import ( "context" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -43,12 +44,14 @@ type Manager struct { containerID func(string) string db *pgxpool.Pool queries *dbsqlc.Queries + logger *slog.Logger } -func NewManager(service ctr.Service, cfg config.MCPConfig) *Manager { +func NewManager(log *slog.Logger, service ctr.Service, cfg config.MCPConfig) *Manager { return &Manager{ service: service, cfg: cfg, + logger: log.With(slog.String("manager", "mcp")), containerID: func(userID string) string { return ContainerPrefix + userID }, diff --git a/internal/memory/llm_client.go b/internal/memory/llm_client.go index 37d56477..3f0004c8 100644 --- a/internal/memory/llm_client.go +++ b/internal/memory/llm_client.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "strings" "time" @@ -15,10 +16,11 @@ type LLMClient struct { baseURL string apiKey string model string + logger *slog.Logger http *http.Client } -func NewLLMClient(baseURL, apiKey, model string, timeout time.Duration) *LLMClient { +func NewLLMClient(log *slog.Logger, baseURL, apiKey, model string, timeout time.Duration) *LLMClient { if baseURL == "" { baseURL = "https://api.openai.com/v1" } @@ -33,6 +35,7 @@ func NewLLMClient(baseURL, apiKey, model string, timeout time.Duration) *LLMClie baseURL: baseURL, apiKey: apiKey, model: model, + logger: log.With(slog.String("client", "llm")), http: &http.Client{ Timeout: timeout, }, diff --git a/internal/memory/qdrant_store.go b/internal/memory/qdrant_store.go index b425ad99..9159d0bc 100644 --- a/internal/memory/qdrant_store.go +++ b/internal/memory/qdrant_store.go @@ -3,6 +3,7 @@ package memory import ( "context" "fmt" + "log/slog" "net/url" "strconv" "strings" @@ -12,13 +13,14 @@ import ( ) type QdrantStore struct { - client *qdrant.Client - collection string - dimension int - baseURL string - apiKey string - timeout time.Duration - vectorNames map[string]int + client *qdrant.Client + collection string + dimension int + baseURL string + apiKey string + timeout time.Duration + logger *slog.Logger + vectorNames map[string]int usesNamedVectors bool } @@ -29,7 +31,7 @@ type qdrantPoint struct { Payload map[string]interface{} `json:"payload,omitempty"` } -func NewQdrantStore(baseURL, apiKey, collection string, dimension int, timeout time.Duration) (*QdrantStore, error) { +func NewQdrantStore(log *slog.Logger, baseURL, apiKey, collection string, dimension int, timeout time.Duration) (*QdrantStore, error) { host, port, useTLS, err := parseQdrantEndpoint(baseURL) if err != nil { return nil, err @@ -59,6 +61,7 @@ func NewQdrantStore(baseURL, apiKey, collection string, dimension int, timeout t baseURL: baseURL, apiKey: apiKey, timeout: timeoutOrDefault(timeout), + logger: log.With(slog.String("store", "qdrant")), } ctx, cancel := context.WithTimeout(context.Background(), timeoutOrDefault(timeout)) @@ -70,10 +73,10 @@ func NewQdrantStore(baseURL, apiKey, collection string, dimension int, timeout t } func (s *QdrantStore) NewSibling(collection string, dimension int) (*QdrantStore, error) { - return NewQdrantStore(s.baseURL, s.apiKey, collection, dimension, s.timeout) + return NewQdrantStore(s.logger, s.baseURL, s.apiKey, collection, dimension, s.timeout) } -func NewQdrantStoreWithVectors(baseURL, apiKey, collection string, vectors map[string]int, timeout time.Duration) (*QdrantStore, error) { +func NewQdrantStoreWithVectors(log *slog.Logger, baseURL, apiKey, collection string, vectors map[string]int, timeout time.Duration) (*QdrantStore, error) { host, port, useTLS, err := parseQdrantEndpoint(baseURL) if err != nil { return nil, err @@ -97,12 +100,13 @@ func NewQdrantStoreWithVectors(baseURL, apiKey, collection string, vectors map[s } store := &QdrantStore{ - client: client, - collection: collection, - baseURL: baseURL, - apiKey: apiKey, - timeout: timeoutOrDefault(timeout), - vectorNames: vectors, + client: client, + collection: collection, + baseURL: baseURL, + apiKey: apiKey, + timeout: timeoutOrDefault(timeout), + logger: log.With(slog.String("store", "qdrant")), + vectorNames: vectors, usesNamedVectors: true, } diff --git a/internal/memory/service.go b/internal/memory/service.go index 1331537f..155f0176 100644 --- a/internal/memory/service.go +++ b/internal/memory/service.go @@ -5,6 +5,7 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "log/slog" "math" "sort" "strings" @@ -20,16 +21,18 @@ type Service struct { embedder embeddings.Embedder store *QdrantStore resolver *embeddings.Resolver + logger *slog.Logger defaultTextModelID string defaultMultimodalModelID string } -func NewService(llm LLM, embedder embeddings.Embedder, store *QdrantStore, resolver *embeddings.Resolver, defaultTextModelID, defaultMultimodalModelID string) *Service { +func NewService(log *slog.Logger, llm LLM, embedder embeddings.Embedder, store *QdrantStore, resolver *embeddings.Resolver, defaultTextModelID, defaultMultimodalModelID string) *Service { return &Service{ llm: llm, embedder: embedder, store: store, resolver: resolver, + logger: log.With(slog.String("service", "memory")), defaultTextModelID: defaultTextModelID, defaultMultimodalModelID: defaultMultimodalModelID, } diff --git a/internal/models/models.go b/internal/models/models.go index 6095522e..92ddd546 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -3,6 +3,7 @@ package models import ( "context" "fmt" + "log/slog" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" @@ -12,12 +13,14 @@ import ( // Service provides CRUD operations for models type Service struct { queries *sqlc.Queries + logger *slog.Logger } // NewService creates a new models service -func NewService(queries *sqlc.Queries) *Service { +func NewService(log *slog.Logger, queries *sqlc.Queries) *Service { return &Service{ queries: queries, + logger: log.With(slog.String("service", "models")), } } diff --git a/internal/providers/service.go b/internal/providers/service.go index bf12ec2c..2c842e37 100644 --- a/internal/providers/service.go +++ b/internal/providers/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "strings" "github.com/google/uuid" @@ -15,11 +16,15 @@ import ( // Service handles provider operations type Service struct { queries *sqlc.Queries + logger *slog.Logger } // NewService creates a new provider service -func NewService(queries *sqlc.Queries) *Service { - return &Service{queries: queries} +func NewService(log *slog.Logger, queries *sqlc.Queries) *Service { + return &Service{ + queries: queries, + logger: log.With(slog.String("service", "providers")), + } } // Create creates a new LLM provider diff --git a/internal/schedule/service.go b/internal/schedule/service.go index 3fe0241b..b7f92eb3 100644 --- a/internal/schedule/service.go +++ b/internal/schedule/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "strings" "sync" "time" @@ -24,11 +25,12 @@ type Service struct { parser cron.Parser chat *chat.Resolver jwtSecret string + logger *slog.Logger mu sync.Mutex jobs map[string]cron.EntryID } -func NewService(queries *sqlc.Queries, chatResolver *chat.Resolver, jwtSecret string) *Service { +func NewService(log *slog.Logger, queries *sqlc.Queries, chatResolver *chat.Resolver, jwtSecret string) *Service { parser := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) c := cron.New(cron.WithParser(parser)) service := &Service{ @@ -37,6 +39,7 @@ func NewService(queries *sqlc.Queries, chatResolver *chat.Resolver, jwtSecret st parser: parser, chat: chatResolver, jwtSecret: jwtSecret, + logger: log.With(slog.String("service", "schedule")), jobs: map[string]cron.EntryID{}, } c.Start() diff --git a/internal/server/server.go b/internal/server/server.go index a40d7880..1eb183e5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,6 +1,7 @@ package server import ( + "log/slog" "strings" "github.com/labstack/echo/v4" @@ -11,11 +12,12 @@ import ( ) type Server struct { - echo *echo.Echo - addr string + echo *echo.Echo + addr string + logger *slog.Logger } -func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, authHandler *handlers.AuthHandler, memoryHandler *handlers.MemoryHandler, embeddingsHandler *handlers.EmbeddingsHandler, chatHandler *handlers.ChatHandler, swaggerHandler *handlers.SwaggerHandler, providersHandler *handlers.ProvidersHandler, modelsHandler *handlers.ModelsHandler, settingsHandler *handlers.SettingsHandler, historyHandler *handlers.HistoryHandler, scheduleHandler *handlers.ScheduleHandler, subagentHandler *handlers.SubagentHandler, containerdHandler *handlers.ContainerdHandler) *Server { +func NewServer(log *slog.Logger, addr string, jwtSecret string, pingHandler *handlers.PingHandler, authHandler *handlers.AuthHandler, memoryHandler *handlers.MemoryHandler, embeddingsHandler *handlers.EmbeddingsHandler, chatHandler *handlers.ChatHandler, swaggerHandler *handlers.SwaggerHandler, providersHandler *handlers.ProvidersHandler, modelsHandler *handlers.ModelsHandler, settingsHandler *handlers.SettingsHandler, historyHandler *handlers.HistoryHandler, scheduleHandler *handlers.ScheduleHandler, subagentHandler *handlers.SubagentHandler, containerdHandler *handlers.ContainerdHandler) *Server { if addr == "" { addr = ":8080" } @@ -23,7 +25,21 @@ func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, e := echo.New() e.HideBanner = true e.Use(middleware.Recover()) - e.Use(middleware.RequestLogger()) + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogMethod: true, + LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + log.Info("request", + slog.String("method", v.Method), + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.Duration("latency", v.Latency), + slog.String("remote_ip", c.RealIP()), + ) + return nil + }, + })) e.Use(auth.JWTMiddleware(jwtSecret, func(c echo.Context) bool { path := c.Request().URL.Path if path == "/ping" || path == "/api/swagger.json" || path == "/auth/login" { @@ -76,8 +92,9 @@ func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, } return &Server{ - echo: e, - addr: addr, + echo: e, + addr: addr, + logger: log.With(slog.String("component", "server")), } } diff --git a/internal/settings/service.go b/internal/settings/service.go index cb52f87e..db4f9210 100644 --- a/internal/settings/service.go +++ b/internal/settings/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "strings" "github.com/google/uuid" @@ -15,10 +16,14 @@ import ( type Service struct { queries *sqlc.Queries + logger *slog.Logger } -func NewService(queries *sqlc.Queries) *Service { - return &Service{queries: queries} +func NewService(log *slog.Logger, queries *sqlc.Queries) *Service { + return &Service{ + queries: queries, + logger: log.With(slog.String("service", "settings")), + } } func (s *Service) Get(ctx context.Context, userID string) (Settings, error) { diff --git a/internal/subagent/service.go b/internal/subagent/service.go index 0e743e78..4a8f8426 100644 --- a/internal/subagent/service.go +++ b/internal/subagent/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "strings" "github.com/google/uuid" @@ -16,10 +17,14 @@ import ( type Service struct { queries *sqlc.Queries + logger *slog.Logger } -func NewService(queries *sqlc.Queries) *Service { - return &Service{queries: queries} +func NewService(log *slog.Logger, queries *sqlc.Queries) *Service { + return &Service{ + queries: queries, + logger: log.With(slog.String("service", "subagent")), + } } func (s *Service) Create(ctx context.Context, userID string, req CreateRequest) (Subagent, error) {