package telemetry import ( "context" "encoding/json" "time" otellog "go.opentelemetry.io/otel/log" sdklog "go.opentelemetry.io/otel/sdk/log" ) // LogBridge implements io.Writer and forwards zerolog JSON lines to the // OTel LoggerProvider. It is used as an extra writer in zerolog's MultiWriter // so that logs go to both file and OTLP. type LogBridge struct { provider *sdklog.LoggerProvider } func (b *LogBridge) Write(p []byte) (n int, err error) { var entry map[string]interface{} if err := json.Unmarshal(p, &entry); err != nil { return len(p), nil // skip malformed lines } logger := b.provider.Logger("zerolog") var rec otellog.Record rec.SetTimestamp(time.Now()) if msg, ok := entry["message"].(string); ok { rec.SetBody(otellog.StringValue(msg)) } if lvl, ok := entry["level"].(string); ok { rec.SetSeverity(mapSeverity(lvl)) } // Forward all fields as attributes attrs := make([]otellog.KeyValue, 0, len(entry)) for k, v := range entry { if k == "message" || k == "level" || k == "time" { continue } switch val := v.(type) { case string: attrs = append(attrs, otellog.String(k, val)) case float64: attrs = append(attrs, otellog.Float64(k, val)) case bool: attrs = append(attrs, otellog.Bool(k, val)) default: b, _ := json.Marshal(val) attrs = append(attrs, otellog.String(k, string(b))) } } rec.AddAttributes(attrs...) logger.Emit(context.Background(), rec) return len(p), nil } func mapSeverity(level string) otellog.Severity { switch level { case "trace": return otellog.SeverityTrace case "debug": return otellog.SeverityDebug case "info": return otellog.SeverityInfo case "warn", "warning": return otellog.SeverityWarn case "error": return otellog.SeverityError case "fatal": return otellog.SeverityFatal case "panic": return otellog.SeverityFatal2 default: return otellog.SeverityInfo } }