diff --git a/pkg/observability/log/level.go b/pkg/observability/log/level.go new file mode 100644 index 000000000..878bb8461 --- /dev/null +++ b/pkg/observability/log/level.go @@ -0,0 +1,29 @@ +package log + +// Level represents the log level, from debug to fatal +type Level struct { + level string +} + +var ( + // DebugLevel causes all logs to be logged + DebugLevel = Level{"debug"} + // InfoLevel causes all logs of level info or more severe to be logged + InfoLevel = Level{"info"} + // WarnLevel causes all logs of level warn or more severe to be logged + WarnLevel = Level{"warn"} + // ErrorLevel causes all logs of level error or more severe to be logged + ErrorLevel = Level{"error"} + // FatalLevel causes only logs of level fatal to be logged + FatalLevel = Level{"fatal"} +) + +// String returns the string representation for Level +// +// This is useful when trying to get the string values for Level and mapping level in other external libraries. For example: +// ``` +// trace.SetLogLevel(kvp.String("loglevel", log.DebugLevel.String()) +// ``` +func (l Level) String() string { + return l.level +} diff --git a/pkg/observability/log/log.go b/pkg/observability/log/log.go new file mode 100644 index 000000000..6b7fb6ab6 --- /dev/null +++ b/pkg/observability/log/log.go @@ -0,0 +1,21 @@ +package log + +import ( + "context" + "log/slog" +) + +type Logger interface { + Log(ctx context.Context, level Level, msg string, fields ...slog.Attr) + Debug(msg string, fields ...slog.Attr) + Info(msg string, fields ...slog.Attr) + Warn(msg string, fields ...slog.Attr) + Error(msg string, fields ...slog.Attr) + Fatal(msg string, fields ...slog.Attr) + WithFields(fields ...slog.Attr) Logger + WithError(err error) Logger + Named(name string) Logger + WithLevel(level Level) Logger + Sync() error + Level() Level +} diff --git a/pkg/observability/log/noop_adapter.go b/pkg/observability/log/noop_adapter.go new file mode 100644 index 000000000..eab44eda8 --- /dev/null +++ b/pkg/observability/log/noop_adapter.go @@ -0,0 +1,62 @@ +package log + +import ( + "context" + "log/slog" +) + +type NoopLogger struct{} + +var _ Logger = (*NoopLogger)(nil) + +func NewNoopLogger() *NoopLogger { + return &NoopLogger{} +} + +func (l *NoopLogger) Level() Level { + return DebugLevel +} + +func (l *NoopLogger) Log(_ context.Context, _ Level, _ string, _ ...slog.Attr) { + // No-op +} + +func (l *NoopLogger) Debug(_ string, _ ...slog.Attr) { + // No-op +} + +func (l *NoopLogger) Info(_ string, _ ...slog.Attr) { + // No-op +} + +func (l *NoopLogger) Warn(_ string, _ ...slog.Attr) { + // No-op +} + +func (l *NoopLogger) Error(_ string, _ ...slog.Attr) { + // No-op +} + +func (l *NoopLogger) Fatal(_ string, _ ...slog.Attr) { + // No-op +} + +func (l *NoopLogger) WithFields(_ ...slog.Attr) Logger { + return l +} + +func (l *NoopLogger) WithError(_ error) Logger { + return l +} + +func (l *NoopLogger) Named(_ string) Logger { + return l +} + +func (l *NoopLogger) WithLevel(_ Level) Logger { + return l +} + +func (l *NoopLogger) Sync() error { + return nil +} diff --git a/pkg/observability/log/slog_adapter.go b/pkg/observability/log/slog_adapter.go new file mode 100644 index 000000000..642ca89c2 --- /dev/null +++ b/pkg/observability/log/slog_adapter.go @@ -0,0 +1,103 @@ +package log + +import ( + "context" + "log/slog" +) + +type SlogLogger struct { + logger *slog.Logger + level Level +} + +func NewSlogLogger(logger *slog.Logger, level Level) *SlogLogger { + return &SlogLogger{ + logger: logger, + level: level, + } +} + +func (l *SlogLogger) Level() Level { + return l.level +} + +func (l *SlogLogger) Log(ctx context.Context, level Level, msg string, fields ...slog.Attr) { + slogLevel := convertLevel(level) + l.logger.LogAttrs(ctx, slogLevel, msg, fields...) +} + +func (l *SlogLogger) Debug(msg string, fields ...slog.Attr) { + l.Log(context.Background(), DebugLevel, msg, fields...) +} + +func (l *SlogLogger) Info(msg string, fields ...slog.Attr) { + l.Log(context.Background(), InfoLevel, msg, fields...) +} + +func (l *SlogLogger) Warn(msg string, fields ...slog.Attr) { + l.Log(context.Background(), WarnLevel, msg, fields...) +} + +func (l *SlogLogger) Error(msg string, fields ...slog.Attr) { + l.Log(context.Background(), ErrorLevel, msg, fields...) +} + +func (l *SlogLogger) Fatal(msg string, fields ...slog.Attr) { + l.Log(context.Background(), FatalLevel, msg, fields...) + panic("fatal log called") +} + +func (l *SlogLogger) WithFields(fields ...slog.Attr) Logger { + fieldKvPairs := make([]any, 0, len(fields)*2) + for _, attr := range fields { + k, v := attr.Key, attr.Value + fieldKvPairs = append(fieldKvPairs, k, v.Any()) + } + return &SlogLogger{ + logger: l.logger.With(fieldKvPairs...), + level: l.level, + } +} + +func (l *SlogLogger) WithError(err error) Logger { + return &SlogLogger{ + logger: l.logger.With("error", err.Error()), + level: l.level, + } +} + +func (l *SlogLogger) Named(name string) Logger { + return &SlogLogger{ + logger: l.logger.With("logger", name), + level: l.level, + } +} + +func (l *SlogLogger) WithLevel(level Level) Logger { + return &SlogLogger{ + logger: l.logger, + level: level, + } +} + +func (l *SlogLogger) Sync() error { + // Slog does not require syncing + return nil +} + +func convertLevel(level Level) slog.Level { + switch level { + case DebugLevel: + return slog.LevelDebug + case InfoLevel: + return slog.LevelInfo + case WarnLevel: + return slog.LevelWarn + case ErrorLevel: + return slog.LevelError + case FatalLevel: + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/pkg/observability/observability.go b/pkg/observability/observability.go new file mode 100644 index 000000000..01d75fe80 --- /dev/null +++ b/pkg/observability/observability.go @@ -0,0 +1,15 @@ +package observability + +import ( + "github.com/github/github-mcp-server/pkg/observability/log" +) + +type Exporters struct { + Logger log.Logger +} + +func NewExporters(logger log.Logger) *Exporters { + return &Exporters{ + Logger: logger, + } +}