// Package logging configures the Notification Service process logger and // provides context-aware helpers for trace fields. package logging import ( "context" "fmt" "log/slog" "os" "strings" "galaxy/notification/internal/api/intentstream" "go.opentelemetry.io/otel/trace" ) // New constructs the process-wide JSON logger from level. func New(level string) (*slog.Logger, error) { var slogLevel slog.Level if err := slogLevel.UnmarshalText([]byte(strings.TrimSpace(level))); err != nil { return nil, fmt.Errorf("build logger: %w", err) } return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slogLevel, })), nil } // TraceAttrsFromContext returns slog key-value pairs for the active // OpenTelemetry span when ctx carries a valid span context. func TraceAttrsFromContext(ctx context.Context) []any { if ctx == nil { return nil } spanContext := trace.SpanContextFromContext(ctx) if !spanContext.IsValid() { return nil } return []any{ "otel_trace_id", spanContext.TraceID().String(), "otel_span_id", spanContext.SpanID().String(), } } // NotificationAttrs returns structured notification-identifying log fields. func NotificationAttrs( notificationID string, notificationType intentstream.NotificationType, producer intentstream.Producer, audienceKind intentstream.AudienceKind, idempotencyKey string, requestID string, traceID string, ) []any { attrs := []any{ "notification_id", notificationID, "notification_type", string(notificationType), "producer", string(producer), "audience_kind", string(audienceKind), "idempotency_key", idempotencyKey, } if strings.TrimSpace(requestID) != "" { attrs = append(attrs, "request_id", requestID) } if strings.TrimSpace(traceID) != "" { attrs = append(attrs, "trace_id", traceID) } return attrs } // IntentAttrs returns structured intent-identifying log fields when a durable // notification record does not yet exist. func IntentAttrs(intent intentstream.Intent) []any { attrs := []any{ "notification_type", string(intent.NotificationType), "producer", string(intent.Producer), "audience_kind", string(intent.AudienceKind), "idempotency_key", intent.IdempotencyKey, } if strings.TrimSpace(intent.RequestID) != "" { attrs = append(attrs, "request_id", intent.RequestID) } if strings.TrimSpace(intent.TraceID) != "" { attrs = append(attrs, "trace_id", intent.TraceID) } return attrs } // RouteAttrs returns structured route-identifying log fields. func RouteAttrs( notificationID string, notificationType intentstream.NotificationType, producer intentstream.Producer, audienceKind intentstream.AudienceKind, idempotencyKey string, requestID string, traceID string, routeID string, channel intentstream.Channel, ) []any { attrs := NotificationAttrs(notificationID, notificationType, producer, audienceKind, idempotencyKey, requestID, traceID) attrs = append(attrs, "route_id", routeID, "channel", string(channel), ) return attrs }