ba919305c2
Features: - Unified Go binary running Task Service, Agent Manager, and Orchestrator - SQLite persistence for boards, columns, and tasks - Docker-based agent execution with ACP message streaming - Augment Agent integration with session resumption support - RESTful API for all operations (port 8080) - In-memory event bus for service communication Architecture: - cmd/kandev: Single entry point for all services - internal/task: Task, board, and column management - internal/agent: Docker container lifecycle and ACP streaming - internal/orchestrator: Agent coordination and monitoring - internal/events: In-memory pub/sub event bus Documentation: - README.md: Project overview and quick start - docs/ARCHITECTURE.md: System architecture (current + planned) - docs/GETTING_STARTED.md: Development setup guide - docs/IMPLEMENTATION_PLAN.md: Original microservices plan - backend/NEXT_STEPS.md: Development roadmap
198 lines
5.3 KiB
Go
198 lines
5.3 KiB
Go
// Package logger provides structured logging using go.uber.org/zap.
|
|
package logger
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
// Context keys for extracting values from context.
|
|
type contextKey string
|
|
|
|
const (
|
|
CorrelationIDKey contextKey = "correlation_id"
|
|
RequestIDKey contextKey = "request_id"
|
|
)
|
|
|
|
// LoggingConfig holds the configuration for the logger.
|
|
type LoggingConfig struct {
|
|
Level string `mapstructure:"level"` // debug, info, warn, error
|
|
Format string `mapstructure:"format"` // json, console
|
|
OutputPath string `mapstructure:"output_path"` // stdout, stderr, or file path
|
|
}
|
|
|
|
// Logger wraps zap.Logger to provide structured logging with helper methods.
|
|
type Logger struct {
|
|
zap *zap.Logger
|
|
sugar *zap.SugaredLogger
|
|
fields []zap.Field
|
|
}
|
|
|
|
var (
|
|
defaultLogger *Logger
|
|
defaultLoggerOnce sync.Once
|
|
)
|
|
|
|
// Default returns the global default logger.
|
|
// This logger is initialized with default settings (info level, json format, stdout).
|
|
func Default() *Logger {
|
|
defaultLoggerOnce.Do(func() {
|
|
var err error
|
|
defaultLogger, err = NewLogger(LoggingConfig{
|
|
Level: "info",
|
|
Format: "json",
|
|
OutputPath: "stdout",
|
|
})
|
|
if err != nil {
|
|
// Fallback to a basic logger if configuration fails
|
|
zapLogger, _ := zap.NewProduction()
|
|
defaultLogger = &Logger{
|
|
zap: zapLogger,
|
|
sugar: zapLogger.Sugar(),
|
|
}
|
|
}
|
|
})
|
|
return defaultLogger
|
|
}
|
|
|
|
// SetDefault sets the global default logger.
|
|
func SetDefault(logger *Logger) {
|
|
defaultLogger = logger
|
|
}
|
|
|
|
// NewLogger creates a new Logger with the given configuration.
|
|
func NewLogger(cfg LoggingConfig) (*Logger, error) {
|
|
level, err := parseLevel(cfg.Level)
|
|
if err != nil {
|
|
level = zapcore.InfoLevel
|
|
}
|
|
|
|
encoderConfig := zap.NewProductionEncoderConfig()
|
|
encoderConfig.TimeKey = "timestamp"
|
|
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
|
encoderConfig.EncodeLevel = zapcore.LowercaseLevelEncoder
|
|
|
|
var encoder zapcore.Encoder
|
|
if cfg.Format == "console" {
|
|
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
|
encoder = zapcore.NewConsoleEncoder(encoderConfig)
|
|
} else {
|
|
encoder = zapcore.NewJSONEncoder(encoderConfig)
|
|
}
|
|
|
|
var writeSyncer zapcore.WriteSyncer
|
|
switch cfg.OutputPath {
|
|
case "", "stdout":
|
|
writeSyncer = zapcore.AddSync(os.Stdout)
|
|
case "stderr":
|
|
writeSyncer = zapcore.AddSync(os.Stderr)
|
|
default:
|
|
file, err := os.OpenFile(cfg.OutputPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writeSyncer = zapcore.AddSync(file)
|
|
}
|
|
|
|
core := zapcore.NewCore(encoder, writeSyncer, level)
|
|
zapLogger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
|
|
|
return &Logger{
|
|
zap: zapLogger,
|
|
sugar: zapLogger.Sugar(),
|
|
}, nil
|
|
}
|
|
|
|
func parseLevel(level string) (zapcore.Level, error) {
|
|
var l zapcore.Level
|
|
err := l.UnmarshalText([]byte(level))
|
|
return l, err
|
|
}
|
|
|
|
// Sync flushes any buffered log entries.
|
|
func (l *Logger) Sync() error {
|
|
return l.zap.Sync()
|
|
}
|
|
|
|
// WithFields returns a new Logger with the given fields added.
|
|
func (l *Logger) WithFields(fields ...zap.Field) *Logger {
|
|
return &Logger{
|
|
zap: l.zap.With(fields...),
|
|
sugar: l.zap.With(fields...).Sugar(),
|
|
fields: append(l.fields, fields...),
|
|
}
|
|
}
|
|
|
|
// WithContext returns a new Logger with context values (correlation ID, etc.) added.
|
|
func (l *Logger) WithContext(ctx context.Context) *Logger {
|
|
fields := []zap.Field{}
|
|
|
|
if correlationID, ok := ctx.Value(CorrelationIDKey).(string); ok && correlationID != "" {
|
|
fields = append(fields, zap.String("correlation_id", correlationID))
|
|
}
|
|
if requestID, ok := ctx.Value(RequestIDKey).(string); ok && requestID != "" {
|
|
fields = append(fields, zap.String("request_id", requestID))
|
|
}
|
|
|
|
if len(fields) == 0 {
|
|
return l
|
|
}
|
|
return l.WithFields(fields...)
|
|
}
|
|
|
|
// WithError returns a new Logger with the error field added.
|
|
func (l *Logger) WithError(err error) *Logger {
|
|
return l.WithFields(zap.Error(err))
|
|
}
|
|
|
|
// WithTaskID returns a new Logger with the task_id field added.
|
|
func (l *Logger) WithTaskID(taskID string) *Logger {
|
|
return l.WithFields(zap.String("task_id", taskID))
|
|
}
|
|
|
|
// WithAgentID returns a new Logger with the agent_id field added.
|
|
func (l *Logger) WithAgentID(agentID string) *Logger {
|
|
return l.WithFields(zap.String("agent_id", agentID))
|
|
}
|
|
|
|
// Debug logs a message at debug level with optional structured fields.
|
|
func (l *Logger) Debug(msg string, fields ...zap.Field) {
|
|
l.zap.Debug(msg, fields...)
|
|
}
|
|
|
|
// Info logs a message at info level with optional structured fields.
|
|
func (l *Logger) Info(msg string, fields ...zap.Field) {
|
|
l.zap.Info(msg, fields...)
|
|
}
|
|
|
|
// Warn logs a message at warn level with optional structured fields.
|
|
func (l *Logger) Warn(msg string, fields ...zap.Field) {
|
|
l.zap.Warn(msg, fields...)
|
|
}
|
|
|
|
// Error logs a message at error level with optional structured fields.
|
|
func (l *Logger) Error(msg string, fields ...zap.Field) {
|
|
l.zap.Error(msg, fields...)
|
|
}
|
|
|
|
// Fatal logs a message at fatal level with optional structured fields,
|
|
// then calls os.Exit(1).
|
|
func (l *Logger) Fatal(msg string, fields ...zap.Field) {
|
|
l.zap.Fatal(msg, fields...)
|
|
}
|
|
|
|
// Zap returns the underlying zap.Logger for advanced use cases.
|
|
func (l *Logger) Zap() *zap.Logger {
|
|
return l.zap
|
|
}
|
|
|
|
// Sugar returns the underlying zap.SugaredLogger for printf-style logging.
|
|
func (l *Logger) Sugar() *zap.SugaredLogger {
|
|
return l.sugar
|
|
}
|
|
|