// Package logging configures the gateway structured logger and provides // context-aware helpers for attaching OpenTelemetry trace identifiers. package logging import ( "context" "strings" "galaxy/gateway/internal/config" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // New constructs the process-wide JSON logger from cfg. func New(cfg config.LoggingConfig) (*zap.Logger, error) { level := zap.NewAtomicLevel() if err := level.UnmarshalText([]byte(strings.TrimSpace(cfg.Level))); err != nil { return nil, err } zapCfg := zap.NewProductionConfig() zapCfg.Level = level zapCfg.Sampling = nil zapCfg.Encoding = "json" zapCfg.EncoderConfig.TimeKey = "timestamp" zapCfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder zapCfg.OutputPaths = []string{"stdout"} zapCfg.ErrorOutputPaths = []string{"stderr"} return zapCfg.Build() } // TraceFieldsFromContext returns zap fields for the active OpenTelemetry span // when ctx carries a valid span context. func TraceFieldsFromContext(ctx context.Context) []zap.Field { if ctx == nil { return nil } spanContext := trace.SpanContextFromContext(ctx) if !spanContext.IsValid() { return nil } return []zap.Field{ zap.String("otel_trace_id", spanContext.TraceID().String()), zap.String("otel_span_id", spanContext.SpanID().String()), } } // Sync flushes logger and ignores the benign stdout or stderr sync errors // commonly returned by containerized or redirected process outputs. func Sync(logger *zap.Logger) error { if logger == nil { return nil } err := logger.Sync() if err == nil || isIgnorableSyncError(err) { return nil } return err } func isIgnorableSyncError(err error) bool { if err == nil { return false } message := strings.ToLower(err.Error()) switch { case strings.Contains(message, "invalid argument"): return true case strings.Contains(message, "bad file descriptor"): return true case strings.Contains(message, "inappropriate ioctl for device"): return true default: return false } }