diff --git a/README.md b/README.md index 42d9f04..ca4ccba 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,5 @@ Open-telemetry extensions for go language | Logger | Version | |----------------------|---------| -| [otelslog](otelslog) | v0.0.1 | \ No newline at end of file +| [otelslog](otelslog) | v0.0.1 | +| [otelzap](otelzap) | v0.1.1 | diff --git a/otelzap/CHANGELOG b/otelzap/CHANGELOG new file mode 100644 index 0000000..162f9eb --- /dev/null +++ b/otelzap/CHANGELOG @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [v0.1.1] + +### Changed + +- opentelemetry-go updated to 1.17.0 +- opentelemetry-logs-go updated to 0.2.0 +- zap updated to 1.25.0 + +## [v0.1.0] 2023-08-06 + +### Fixed + +- using pointer to struct instead of raw struct + +### Changed + +- github.com/agoda-com/opentelemetry-logs-go updated to v0.1.2 + +## [v0.0.1] 2023-07-25 + +### Added + +- Otel Zap Core with OTLP format Log Records export +- Ctx(ctx) method to provide context to underlying exporters + diff --git a/otelzap/README.md b/otelzap/README.md new file mode 100644 index 0000000..8a4eca4 --- /dev/null +++ b/otelzap/README.md @@ -0,0 +1,77 @@ +# otelzap + +Zap logger with OpenTelemetry support. This logger will export LogRecord's in OTLP format. + +## Quick start + +[Export env variable](https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_endpoint) `OTEL_EXPORTER_OTLP_ENDPOINT=https://localhost:4318` +to your OTLP collector + +```go +package main + +import ( + "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + semconv2 "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.uber.org/zap" + "github.com/agoda-com/otelzap" + otellogs "github.com/agoda-com/opentelemetry-logs-go" + sdk "github.com/agoda-com/opentelemetry-logs-go/sdk/logs" + "github.com/agoda-com/opentelemetry-logs-go/exporters/otlp/otlplogs" + "github.com/agoda-com/opentelemetry-logs-go/exporters/otlp/otlplogs/otlplogshttp" + "os" +) + +// configure common attributes for all logs +func newResource() *resource.Resource { + hostName, _ := os.Hostname() + return resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("otelzap-example"), + semconv.ServiceVersion("1.0.0"), + semconv.HostName(hostName), + ) +} + +func main() { + + ctx := context.Background() + + // configure opentelemetry logger provider + logExporter, _ := otlplogs.NewExporter(ctx) + loggerProvider := sdk.NewLoggerProvider( + sdk.WithBatcher(logExporter), + sdk.WithResource(newResource()), + ) + // gracefully shutdown logger to flush accumulated signals before program finish + defer loggerProvider.Shutdown(ctx) + + // set opentelemetry logger provider globally + otellogs.SetLoggerProvider(loggerProvider) + + // create new logger with opentelemetry zap core and set it globally + logger := zap.New(otelzap.NewOtelCore(loggerProvider)) + zap.ReplaceGlobals(logger) + + // now your application ready to produce logs to opentelemetry collector + doSomething() + +} + +func doSomething() { + // start new span + // see official trace documentation https://github.com/open-telemetry/opentelemetry-go + tracer := otel.Tracer("my-tracer") + spanCtx, span := tracer.Start(context.Background(), "My Span") + defer func() { + span.End() + }() + + // send log with opentelemetry context + otelzap.Ctx(spanCtx).Info("My message with trace context") +} + +``` diff --git a/otelzap/conv.go b/otelzap/conv.go new file mode 100644 index 0000000..8d29cec --- /dev/null +++ b/otelzap/conv.go @@ -0,0 +1,88 @@ +/* +Copyright Agoda Services Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package otelzap + +import ( + otel "github.com/agoda-com/opentelemetry-logs-go/logs" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + "go.uber.org/zap/zapcore" + "math" +) + +// otelLevel zap level to otlp level converter +func otelLevel(level zapcore.Level) otel.SeverityNumber { + switch level { + case zapcore.DebugLevel: + return otel.DEBUG + case zapcore.InfoLevel: + return otel.INFO + case zapcore.WarnLevel: + return otel.WARN + case zapcore.ErrorLevel: + return otel.ERROR + case zapcore.DPanicLevel: + return otel.ERROR + case zapcore.PanicLevel: + return otel.ERROR + case zapcore.FatalLevel: + return otel.FATAL + } + return otel.TRACE +} + +// otelAttribute convert zap Field into OpenTelemetry Attribute +func otelAttribute(f zapcore.Field) []attribute.KeyValue { + switch f.Type { + case zapcore.UnknownType: + return []attribute.KeyValue{attribute.String(f.Key, f.String)} + case zapcore.BoolType: + return []attribute.KeyValue{attribute.Bool(f.Key, f.Integer == 1)} + case zapcore.Float64Type: + return []attribute.KeyValue{attribute.Float64(f.Key, math.Float64frombits(uint64(f.Integer)))} + case zapcore.Float32Type: + return []attribute.KeyValue{attribute.Float64(f.Key, math.Float64frombits(uint64(f.Integer)))} + case zapcore.Int64Type: + return []attribute.KeyValue{attribute.Int64(f.Key, f.Integer)} + case zapcore.Int32Type: + return []attribute.KeyValue{attribute.Int64(f.Key, f.Integer)} + case zapcore.Int16Type: + return []attribute.KeyValue{attribute.Int64(f.Key, f.Integer)} + case zapcore.Int8Type: + return []attribute.KeyValue{attribute.Int64(f.Key, f.Integer)} + case zapcore.StringType: + return []attribute.KeyValue{attribute.String(f.Key, f.String)} + case zapcore.Uint64Type: + return []attribute.KeyValue{attribute.Int64(f.Key, int64(uint64(f.Integer)))} + case zapcore.Uint32Type: + return []attribute.KeyValue{attribute.Int64(f.Key, int64(uint64(f.Integer)))} + case zapcore.Uint16Type: + return []attribute.KeyValue{attribute.Int64(f.Key, int64(uint64(f.Integer)))} + case zapcore.Uint8Type: + return []attribute.KeyValue{attribute.Int64(f.Key, int64(uint64(f.Integer)))} + case zapcore.ErrorType: + err := f.Interface.(error) + if err != nil { + return []attribute.KeyValue{semconv.ExceptionMessage(err.Error())} + } + return []attribute.KeyValue{} + case zapcore.SkipType: + return []attribute.KeyValue{} + } + // unhandled types will be treated as string + return []attribute.KeyValue{attribute.String(f.Key, f.String)} +} diff --git a/otelzap/core.go b/otelzap/core.go new file mode 100644 index 0000000..7df065f --- /dev/null +++ b/otelzap/core.go @@ -0,0 +1,134 @@ +/* +Copyright Agoda Services Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package otelzap + +import ( + otel "github.com/agoda-com/opentelemetry-logs-go/logs" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap/zapcore" +) + +const ( + instrumentationName = "github.com/agoda-com/opentelemetry-go/otelzap" +) + +// This class provide interface for OTLP logger +type otlpCore struct { + logger otel.Logger + + fields []zapcore.Field +} + +var instrumentationScope = instrumentation.Scope{ + Name: instrumentationName, + Version: Version(), + SchemaURL: semconv.SchemaURL, +} + +func (otlpCore) Enabled(zapcore.Level) bool { + return true +} + +func (c *otlpCore) With(f []zapcore.Field) zapcore.Core { + fields := c.fields + fields = append(fields, f...) + + return &otlpCore{ + logger: c.logger, + fields: fields, + } +} + +// Check OTLP zap extension method to check if logger is enabled +func (c *otlpCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(entry.Level) { + return checked.AddCore(entry, c) + } + return checked +} + +func (c *otlpCore) Sync() error { + return nil +} + +func (c *otlpCore) Write(ent zapcore.Entry, fields []zapcore.Field) error { + var attributes []attribute.KeyValue + var spanCtx *trace.SpanContext + + // add common zap log fields as attributes + for _, s := range c.fields { + if s.Key == "context" { + if ctxValue, ok := s.Interface.(trace.SpanContext); ok { + spanCtx = &ctxValue + } + } else { + attributes = append(attributes, otelAttribute(s)...) + } + } + // add zap log fields as attributes + for _, s := range fields { + attributes = append(attributes, otelAttribute(s)...) + } + + if ent.Level > zapcore.InfoLevel { + callerString := ent.Caller.String() + + if len(callerString) > 0 { + attributes = append(attributes, semconv.ExceptionType(callerString)) + } + + if len(ent.Stack) > 0 { + attributes = append(attributes, semconv.ExceptionStacktrace(ent.Stack)) + } + } + + severityString := ent.Level.String() + severity := otelLevel(ent.Level) + + var traceID *trace.TraceID = nil + var spanID *trace.SpanID = nil + var traceFlags *trace.TraceFlags = nil + if spanCtx != nil { + tid := spanCtx.TraceID() + sid := spanCtx.SpanID() + tf := spanCtx.TraceFlags() + traceID = &tid + spanID = &sid + traceFlags = &tf + } + + lrc := otel.LogRecordConfig{ + Timestamp: &ent.Time, + ObservedTimestamp: ent.Time, + TraceId: traceID, + SpanId: spanID, + TraceFlags: traceFlags, + SeverityText: &severityString, + SeverityNumber: &severity, + Body: &ent.Message, + Resource: nil, + InstrumentationScope: &instrumentationScope, + Attributes: &attributes, + } + + r := otel.NewLogRecord(lrc) + c.logger.Emit(r) + return nil +} diff --git a/otelzap/global.go b/otelzap/global.go new file mode 100644 index 0000000..c77cab1 --- /dev/null +++ b/otelzap/global.go @@ -0,0 +1,38 @@ +/* +Copyright Agoda Services Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package otelzap + +import ( + "context" + "go.uber.org/zap" +) + +// L returns the global Logger +func L() *Logger { + return &Logger{ + zap.L(), + } +} + +func S() *SugaredLogger { + return L().Sugar() +} + +// Ctx is a shortcut for L().Ctx(ctx). +func Ctx(ctx context.Context) *Logger { + return L().Ctx(ctx) +} diff --git a/otelzap/go.mod b/otelzap/go.mod new file mode 100644 index 0000000..3dbd691 --- /dev/null +++ b/otelzap/go.mod @@ -0,0 +1,19 @@ +module github.com/agoda-com/opentelemetry-go/otelzap + +go 1.20 + +require ( + github.com/agoda-com/opentelemetry-logs-go v0.2.0 + go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel/sdk v1.17.0 + go.opentelemetry.io/otel/trace v1.17.0 + go.uber.org/zap v1.25.0 +) + +require ( + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/otel/metric v1.17.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/sys v0.11.0 // indirect +) \ No newline at end of file diff --git a/otelzap/go.sum b/otelzap/go.sum new file mode 100644 index 0000000..ebb0743 --- /dev/null +++ b/otelzap/go.sum @@ -0,0 +1,11 @@ +github.com/agoda-com/opentelemetry-logs-go v0.2.0/go.mod h1:bjUiOkH82zqgjLmxk0v9axmV5b9Ty+1qlixsrAMTLwQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= +go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/otelzap/logger.go b/otelzap/logger.go new file mode 100644 index 0000000..2c81b36 --- /dev/null +++ b/otelzap/logger.go @@ -0,0 +1,68 @@ +/* +Copyright Agoda Services Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package otelzap + +import ( + "context" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Logger is a thin wrapper for zap.Logger that adds Ctx method. +type Logger struct { + *zap.Logger +} + +const contextKey = "context" + +func (l *Logger) Sugar() *SugaredLogger { + return &SugaredLogger{ + SugaredLogger: l.Logger.Sugar(), + } +} + +func (l *Logger) Ctx(ctx context.Context) *Logger { + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + return l.With(zap.Reflect(contextKey, span.SpanContext())) + } + return l +} + +func (l *Logger) With(fields ...zapcore.Field) *Logger { + return &Logger{ + Logger: l.Logger.With(fields...), + } +} + +type SugaredLogger struct { + *zap.SugaredLogger +} + +func (l *SugaredLogger) Ctx(ctx context.Context) *SugaredLogger { + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + return &SugaredLogger{ + SugaredLogger: l.With(zap.Reflect(contextKey, span.SpanContext())), + } + } + return &SugaredLogger{ + SugaredLogger: l.SugaredLogger, + } +} diff --git a/otelzap/otelzap.go b/otelzap/otelzap.go new file mode 100644 index 0000000..dad6353 --- /dev/null +++ b/otelzap/otelzap.go @@ -0,0 +1,34 @@ +/* +Copyright Agoda Services Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package otelzap + +import ( + otel "github.com/agoda-com/opentelemetry-logs-go/logs" + "go.uber.org/zap/zapcore" +) + +// NewOtelCore creates new OpenTelemetry Core to export logs in OTLP format +func NewOtelCore(loggerProvider otel.LoggerProvider) zapcore.Core { + logger := loggerProvider.Logger( + instrumentationScope.Name, + otel.WithInstrumentationVersion(instrumentationScope.Version), + ) + + return &otlpCore{ + logger: logger, + } +} diff --git a/otelzap/version.go b/otelzap/version.go new file mode 100644 index 0000000..346d9c4 --- /dev/null +++ b/otelzap/version.go @@ -0,0 +1,22 @@ +/* +Copyright Agoda Services Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package otelzap + +// Version is the current release version of OpenTelemetry Zap in use. +func Version() string { + return "0.1.1" +}