-
Notifications
You must be signed in to change notification settings - Fork 25
INFOPLAT-1035: logger implement otelzap #1359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0259a03
e17ee3a
ff4051c
d5fb5bb
d317261
f681031
9173c4a
7160a95
6c3afc6
44bfe23
b6ebf02
0ab7d2e
5ca7662
8260585
3d0a044
051722f
19b7ec9
4547d57
e205c53
3b9806b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -102,6 +102,12 @@ func NewWithSync(w io.Writer) Logger { | |||||||||||||||||||||||||||
| return &logger{zap.New(core).Sugar()} | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // NewWithCores returns a new Logger with one or more zapcore.Core. | ||||||||||||||||||||||||||||
| // If multiple cores are provided, they are combined using zapcore.NewTee. | ||||||||||||||||||||||||||||
| func NewWithCores(cores ...zapcore.Core) Logger { | ||||||||||||||||||||||||||||
| return &logger{zap.New(zapcore.NewTee(cores...)).Sugar()} | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: In practice the LoggerProvider/Logger will come from beholder.Client. You could create this helper here
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's separate these concerns. All that this package API needs to do is allow a caller to pass in a zap.Core. It doesn't need to know about otel at all. // NewWithCore returns a new Logger with a given zapcore.Core.
func NewWithCore(core zapcore.Core) Logger {
return &logger{zap.New(core).Sugar()}
}Perhaps we would rather accept variadic cores than just one, but either way they should be combined via https://pkg.go.dev/go.uber.org/zap/zapcore#NewTee The more interesting question is whether we always want to include the default core or provide a way for the caller to get it so it's optional. lggr := logger.NewWithCores(otellogger.NewCore())vs. lggr := logger.NewWithCores(logger.NewZapCore(), otellog.NewZapCore()) |
||||||||||||||||||||||||||||
| // Test returns a new test Logger for tb. | ||||||||||||||||||||||||||||
| func Test(tb testing.TB) Logger { | ||||||||||||||||||||||||||||
| cfg := zap.NewDevelopmentEncoderConfig() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| package otelzap | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "math" | ||
| "time" | ||
|
|
||
| "go.opentelemetry.io/otel/attribute" | ||
| otellog "go.opentelemetry.io/otel/log" | ||
| semconv "go.opentelemetry.io/otel/semconv/v1.17.0" | ||
| oteltrace "go.opentelemetry.io/otel/trace" | ||
|
|
||
| "go.uber.org/zap/zapcore" | ||
| ) | ||
|
|
||
| // OtelZapCore is a zapcore.Core implementation that exports logs to OpenTelemetry | ||
| // It implements the zapcore.Core interface and uses OpenTelemetry's logging API | ||
| type OtelZapCore struct { | ||
| logger otellog.Logger | ||
| fields []zapcore.Field | ||
| levelEnabler zapcore.LevelEnabler | ||
| } | ||
|
|
||
| type Option func(c *OtelZapCore) | ||
|
|
||
| // NewOtelCore initializes an OpenTelemetry Core for exporting logs in OTLP format | ||
| func NewCore(logger otellog.Logger, opts ...Option) zapcore.Core { | ||
|
|
||
| c := &OtelZapCore{ | ||
| logger: logger, | ||
| levelEnabler: zapcore.InfoLevel, | ||
| } | ||
| for _, apply := range opts { | ||
| apply(c) | ||
| } | ||
|
|
||
| return c | ||
| } | ||
|
|
||
| // Enabled checks if the given log level is enabled for the OpenTelemetry Core | ||
| func (o OtelZapCore) Enabled(level zapcore.Level) bool { | ||
| return o.levelEnabler.Enabled(level) | ||
| } | ||
|
|
||
| // With returns a new OpenTelemetry Core with the given fields added to the log entry | ||
| func (o OtelZapCore) With(fields []zapcore.Field) zapcore.Core { | ||
| newFields := append([]zapcore.Field{}, o.fields...) | ||
| newFields = append(newFields, fields...) | ||
|
|
||
| return &OtelZapCore{ | ||
| logger: o.logger, | ||
| fields: newFields, | ||
| levelEnabler: o.levelEnabler, | ||
| } | ||
| } | ||
|
|
||
| // Check checks if the given log entry is enabled for the OpenTelemetry Core | ||
| func (o OtelZapCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { | ||
| if o.Enabled(entry.Level) { | ||
| checked = checked.AddCore(entry, o) | ||
| } | ||
| return checked | ||
| } | ||
|
|
||
| func (o OtelZapCore) Sync() error { | ||
| // If no zap core is set, we don't need to sync anything | ||
| // as OpenTelemetry Core does not have a sync operation. | ||
| return nil | ||
| } | ||
|
|
||
| // WithLevel sets the minimum level for the OpenTelemetry Core log to be exported | ||
| func WithLevel(level zapcore.Level) Option { | ||
| return Option(func(o *OtelZapCore) { | ||
| o.levelEnabler = level | ||
| }) | ||
| } | ||
|
|
||
| // WithLevelEnabler sets the zapcore.LevelEnabler for determining which log levels to export | ||
| func WithLevelEnabler(levelEnabler zapcore.LevelEnabler) Option { | ||
| return Option(func(o *OtelZapCore) { | ||
| o.levelEnabler = levelEnabler | ||
| }) | ||
| } | ||
|
|
||
| func (o OtelZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { | ||
| var attributes []attribute.KeyValue | ||
| var spanCtx *oteltrace.SpanContext | ||
|
|
||
| // Add core-attached fields | ||
| for _, f := range o.fields { | ||
| if f.Key == "context" { | ||
| if ctxValue, ok := f.Interface.(oteltrace.SpanContext); ok { | ||
| spanCtx = &ctxValue | ||
| } | ||
| } else { | ||
| attributes = append(attributes, mapZapField(f)) | ||
| } | ||
| } | ||
|
|
||
| // Add fields passed during log call | ||
| for _, f := range fields { | ||
| attributes = append(attributes, mapZapField(f)) | ||
| } | ||
|
|
||
| // Add exception metadata | ||
| if entry.Level > zapcore.InfoLevel { | ||
| if entry.Caller.Defined { | ||
| attributes = append(attributes, semconv.ExceptionType(entry.Caller.String())) | ||
| } | ||
| if entry.Stack != "" { | ||
| attributes = append(attributes, semconv.ExceptionStacktrace(entry.Stack)) | ||
| } | ||
| } | ||
|
|
||
| // Add span context attributes if available | ||
| if spanCtx != nil { | ||
| if spanCtx.TraceID().IsValid() { | ||
| attributes = append(attributes, attribute.String("trace_id", spanCtx.TraceID().String())) | ||
| } | ||
| if spanCtx.SpanID().IsValid() { | ||
| attributes = append(attributes, attribute.String("span_id", spanCtx.SpanID().String())) | ||
| } | ||
| attributes = append(attributes, attribute.String("trace_flags", spanCtx.TraceFlags().String())) | ||
| } | ||
|
|
||
| // Convert to OpenTelemetry KeyValues and emit | ||
| otelAttrs := make([]otellog.KeyValue, len(attributes)) | ||
| for i, attr := range attributes { | ||
| otelAttrs[i] = otellog.KeyValue{ | ||
| Key: string(attr.Key), | ||
| Value: mapAttrValueToLogAttrValue(attr.Value), | ||
| } | ||
| } | ||
|
|
||
| logRecord := otellog.Record{} | ||
| logRecord.SetBody(otellog.StringValue(entry.Message)) | ||
| logRecord.SetSeverity(mapZapSeverity(entry.Level)) | ||
| logRecord.SetSeverityText(entry.Level.String()) | ||
| logRecord.SetTimestamp(entry.Time) | ||
| logRecord.SetObservedTimestamp(time.Now()) | ||
| logRecord.AddAttributes(otelAttrs...) | ||
|
|
||
| o.logger.Emit(context.Background(), logRecord) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func mapZapField(f zapcore.Field) attribute.KeyValue { | ||
| switch f.Type { | ||
| case zapcore.StringType: | ||
| return attribute.String(f.Key, f.String) | ||
|
|
||
| case zapcore.Int64Type, zapcore.Int32Type, zapcore.Int16Type, zapcore.Int8Type: | ||
| return attribute.Int64(f.Key, f.Integer) | ||
|
|
||
| case zapcore.Uint64Type, zapcore.Uint32Type, zapcore.Uint16Type, zapcore.Uint8Type, zapcore.UintptrType: | ||
| return attribute.Int64(f.Key, int64(f.Integer)) | ||
|
|
||
| case zapcore.BoolType: | ||
| return attribute.Bool(f.Key, f.Integer == 1) | ||
|
|
||
| case zapcore.Float64Type: | ||
| return attribute.Float64(f.Key, math.Float64frombits(uint64(f.Integer))) | ||
|
|
||
| case zapcore.ErrorType: | ||
| if err, ok := f.Interface.(error); ok { | ||
| return attribute.String(f.Key, err.Error()) | ||
| } | ||
| return attribute.String(f.Key, "invalid error field") | ||
|
|
||
| case zapcore.StringerType: | ||
| return attribute.String(f.Key, f.Interface.(fmt.Stringer).String()) | ||
|
|
||
| case zapcore.TimeType: | ||
| if t, ok := f.Interface.(time.Time); ok { | ||
| return attribute.String(f.Key, t.Format(time.RFC3339)) | ||
| } | ||
| return attribute.String(f.Key, fmt.Sprintf("invalid time: %v", f.Interface)) | ||
|
|
||
| case zapcore.DurationType: | ||
| if d, ok := f.Interface.(time.Duration); ok { | ||
| return attribute.String(f.Key, d.String()) | ||
| } | ||
| return attribute.String(f.Key, fmt.Sprintf("invalid duration: %v", f.Interface)) | ||
|
|
||
| case zapcore.BinaryType: | ||
| if b, ok := f.Interface.([]byte); ok { | ||
| return attribute.String(f.Key, fmt.Sprintf("binary data: %x", b)) | ||
| } | ||
| return attribute.String(f.Key, fmt.Sprintf("invalid binary: %v", f.Interface)) | ||
|
|
||
| case zapcore.ByteStringType: | ||
| if b, ok := f.Interface.([]byte); ok { | ||
| return attribute.String(f.Key, fmt.Sprintf("byte string: %x", b)) | ||
| } | ||
| return attribute.String(f.Key, fmt.Sprintf("invalid byte string: %v", f.Interface)) | ||
|
|
||
| default: | ||
| return attribute.String(f.Key, f.String) | ||
| } | ||
| } | ||
|
|
||
| func mapZapSeverity(level zapcore.Level) otellog.Severity { | ||
| switch level { | ||
| case zapcore.DebugLevel: | ||
| return otellog.SeverityDebug | ||
| case zapcore.InfoLevel: | ||
| return otellog.SeverityInfo | ||
| case zapcore.WarnLevel: | ||
| return otellog.SeverityWarn | ||
| case zapcore.ErrorLevel: | ||
| return otellog.SeverityError | ||
| case zapcore.DPanicLevel: | ||
| return otellog.SeverityFatal1 | ||
| case zapcore.PanicLevel: | ||
| return otellog.SeverityFatal2 | ||
| case zapcore.FatalLevel: | ||
| return otellog.SeverityFatal3 | ||
| default: | ||
| return otellog.SeverityUndefined | ||
| } | ||
| } | ||
|
|
||
| func mapAttrValueToLogAttrValue(v attribute.Value) otellog.Value { | ||
| switch v.Type() { | ||
| case attribute.STRING: | ||
| return otellog.StringValue(v.AsString()) | ||
| case attribute.BOOL: | ||
| return otellog.BoolValue(v.AsBool()) | ||
| case attribute.INT64: | ||
| return otellog.Int64Value(v.AsInt64()) | ||
| case attribute.FLOAT64: | ||
| return otellog.Float64Value(v.AsFloat64()) | ||
| case attribute.INVALID: | ||
| return otellog.StringValue("invalid") | ||
| case attribute.STRINGSLICE, attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE: | ||
| return otellog.StringValue(fmt.Sprintf("%v", v.AsInterface())) | ||
| default: | ||
| return otellog.StringValue(fmt.Sprintf("%v", v.AsInterface())) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to add some tests for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do we want to test here? This function just returns the logger with zapcore tee functionality