feat: refactor logging system to slog with DI and component tagging

This commit is contained in:
BBQ
2026-01-31 23:23:57 -08:00
committed by GitHub
parent a7700e1959
commit 46d2968e2c
36 changed files with 439 additions and 134 deletions
+65 -47
View File
@@ -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
}
+17 -7
View File
@@ -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)
}
}
+16 -2
View File
@@ -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)
}
}
+4
View File
@@ -1,4 +1,8 @@
## Service configuration
[log]
level = "info"
format = "text"
[server]
# HTTP listen address
addr = ":8080"
+4 -1
View File
@@ -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,
},
+10
View File
@@ -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,
},
+4 -1
View File
@@ -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")),
}
}
+4 -1
View File
@@ -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,
},
+4 -1
View File
@@ -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,
},
+6 -3
View File
@@ -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
+4 -1
View File
@@ -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")),
}
}
+7 -2
View File
@@ -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) {
+8 -3
View File
@@ -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{
+5 -2
View File
@@ -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")),
}
}
+7 -2
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+6 -3
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+6 -3
View File
@@ -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) {
+7 -2
View File
@@ -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) {
+66
View File
@@ -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...) }
+55
View File
@@ -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)
}
}
}
+4 -1
View File
@@ -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
},
+4 -1
View File
@@ -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,
},
+7 -3
View File
@@ -3,6 +3,7 @@ package memory
import (
"context"
"fmt"
"log/slog"
"net/url"
"strconv"
"strings"
@@ -18,6 +19,7 @@ type QdrantStore struct {
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
@@ -102,6 +105,7 @@ func NewQdrantStoreWithVectors(baseURL, apiKey, collection string, vectors map[s
baseURL: baseURL,
apiKey: apiKey,
timeout: timeoutOrDefault(timeout),
logger: log.With(slog.String("store", "qdrant")),
vectorNames: vectors,
usesNamedVectors: true,
}
+4 -1
View File
@@ -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,
}
+4 -1
View File
@@ -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")),
}
}
+7 -2
View File
@@ -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
+4 -1
View File
@@ -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()
+19 -2
View File
@@ -1,6 +1,7 @@
package server
import (
"log/slog"
"strings"
"github.com/labstack/echo/v4"
@@ -13,9 +14,10 @@ import (
type Server struct {
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" {
@@ -78,6 +94,7 @@ func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler,
return &Server{
echo: e,
addr: addr,
logger: log.With(slog.String("component", "server")),
}
}
+7 -2
View File
@@ -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) {
+7 -2
View File
@@ -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) {