From fb316b5c913f77b0fdf2b04c3414d9240257ecae Mon Sep 17 00:00:00 2001 From: Akhigbe Eromosele David Date: Thu, 11 Jul 2024 12:47:14 +0100 Subject: [PATCH] Add skeleton for zerolog bridge (#5782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces the `otelzerolog` package, which provides a `SeverityHook` to bridge the logging capabilities of Zerolog with OpenTelemetry. The following key features are included: ## Changes - Added `config` struct to handle configuration options. - Implemented `Option` interface and related functions (`WithVersion`, `WithSchemaURL`, `WithLoggerProvider`). - Created `NewSeverityHook` function to initialize a `SeverityHook`. - Defined `SeverityHook` struct to perform the logging bridge functionality. Part of https://github.com/open-telemetry/opentelemetry-go-contrib/issues/5405 --------- Co-authored-by: Damien Mathieu <42@dmathieu.com> Co-authored-by: Robert PajÄ…k --- bridges/otelzerolog/example_test.go | 23 ++++++ bridges/otelzerolog/go.mod | 23 ++++++ bridges/otelzerolog/go.sum | 40 +++++++++++ bridges/otelzerolog/hook.go | 104 +++++++++++++++++++++++++++ bridges/otelzerolog/hook_test.go | 105 ++++++++++++++++++++++++++++ versions.yaml | 1 + 6 files changed, 296 insertions(+) create mode 100644 bridges/otelzerolog/example_test.go create mode 100644 bridges/otelzerolog/go.mod create mode 100644 bridges/otelzerolog/go.sum create mode 100644 bridges/otelzerolog/hook.go create mode 100644 bridges/otelzerolog/hook_test.go diff --git a/bridges/otelzerolog/example_test.go b/bridges/otelzerolog/example_test.go new file mode 100644 index 00000000000..2eb869d75c2 --- /dev/null +++ b/bridges/otelzerolog/example_test.go @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelzerolog_test + +import ( + "os" + + "github.com/rs/zerolog" + + "go.opentelemetry.io/contrib/bridges/otelzerolog" + "go.opentelemetry.io/otel/log/noop" +) + +func Example() { + // Use a working LoggerProvider implementation instead e.g. using go.opentelemetry.io/otel/sdk/log. + provider := noop.NewLoggerProvider() + + // Create a logger that emits logs to both STDOUT and the OTel Go SDK. + hook := otelzerolog.NewHook("my/pkg/name", otelzerolog.WithLoggerProvider(provider)) + logger := zerolog.New(os.Stdout).With().Logger() + logger = logger.Hook(hook) +} diff --git a/bridges/otelzerolog/go.mod b/bridges/otelzerolog/go.mod new file mode 100644 index 00000000000..46ab1c044a6 --- /dev/null +++ b/bridges/otelzerolog/go.mod @@ -0,0 +1,23 @@ +module go.opentelemetry.io/contrib/bridges/otelzerolog + +go 1.21 + +require ( + github.com/rs/zerolog v1.33.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel/log v0.3.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/sys v0.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/bridges/otelzerolog/go.sum b/bridges/otelzerolog/go.sum new file mode 100644 index 00000000000..87eeb9f91bc --- /dev/null +++ b/bridges/otelzerolog/go.sum @@ -0,0 +1,40 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/log v0.3.0 h1:kJRFkpUFYtny37NQzL386WbznUByZx186DpEMKhEGZs= +go.opentelemetry.io/otel/log v0.3.0/go.mod h1:ziCwqZr9soYDwGNbIL+6kAvQC+ANvjgG367HVcyR/ys= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/bridges/otelzerolog/hook.go b/bridges/otelzerolog/hook.go new file mode 100644 index 00000000000..2928396240b --- /dev/null +++ b/bridges/otelzerolog/hook.go @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package otelzerolog provides a [Hook], a [zerolog.Hook] implementation that +// can be used to bridge between the [zerolog] API and [OpenTelemetry]. +// [OpenTelemetry]: https://opentelemetry.io/docs/concepts/signals/logs/ +package otelzerolog // import "go.opentelemetry.io/contrib/bridges/otelzerolog" + +import ( + "github.com/rs/zerolog" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/global" +) + +type config struct { + provider log.LoggerProvider + version string + schemaURL string +} + +func newConfig(options []Option) config { + var c config + for _, opt := range options { + c = opt.apply(c) + } + + if c.provider == nil { + c.provider = global.GetLoggerProvider() + } + return c +} + +func (c config) logger(name string) log.Logger { + var opts []log.LoggerOption + if c.version != "" { + opts = append(opts, log.WithInstrumentationVersion(c.version)) + } + if c.schemaURL != "" { + opts = append(opts, log.WithSchemaURL(c.schemaURL)) + } + return c.provider.Logger(name, opts...) +} + +// Option configures a Hook. +type Option interface { + apply(config) config +} +type optFunc func(config) config + +func (f optFunc) apply(c config) config { return f(c) } + +// WithVersion returns an [Option] that configures the version of the +// [log.Logger] used by a [Hook]. The version should be the version of the +// package that is being logged. +func WithVersion(version string) Option { + return optFunc(func(c config) config { + c.version = version + return c + }) +} + +// WithSchemaURL returns an [Option] that configures the semantic convention +// schema URL of the [log.Logger] used by a [Hook]. The schemaURL should be +// the schema URL for the semantic conventions used in log records. +func WithSchemaURL(schemaURL string) Option { + return optFunc(func(c config) config { + c.schemaURL = schemaURL + return c + }) +} + +// WithLoggerProvider returns an [Option] that configures [log.LoggerProvider] +// used by a [Hook]. +// +// By default if this Option is not provided, the Hook will use the global +// LoggerProvider. +func WithLoggerProvider(provider log.LoggerProvider) Option { + return optFunc(func(c config) config { + c.provider = provider + return c + }) +} + +// Hook is a [zerolog.Hook] that sends all logging records it receives to +// OpenTelemetry. See package documentation for how conversions are made. +type Hook struct { + logger log.Logger +} + +// NewHook returns a new [Hook] to be used as a [Zerolog.Hook]. +// If [WithLoggerProvider] is not provided, the returned Hook will use the +// global LoggerProvider. +func NewHook(name string, options ...Option) *Hook { + cfg := newConfig(options) + return &Hook{ + logger: cfg.logger(name), + } +} + +// Run handles the passed record, and sends it to OpenTelemetry. +func (h Hook) Run(e *zerolog.Event, level zerolog.Level, msg string) { + // TODO +} diff --git a/bridges/otelzerolog/hook_test.go b/bridges/otelzerolog/hook_test.go new file mode 100644 index 00000000000..273e9c73c59 --- /dev/null +++ b/bridges/otelzerolog/hook_test.go @@ -0,0 +1,105 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package otelzerolog + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/embedded" + "go.opentelemetry.io/otel/log/global" +) + +type mockLoggerProvider struct { + embedded.LoggerProvider +} + +func (mockLoggerProvider) Logger(name string, options ...log.LoggerOption) log.Logger { + return nil +} + +func TestNewConfig(t *testing.T) { + customLoggerProvider := mockLoggerProvider{} + + for _, tt := range []struct { + name string + options []Option + + wantConfig config + }{ + { + name: "with no options", + wantConfig: config{ + provider: global.GetLoggerProvider(), + }, + }, + { + name: "with a custom version", + options: []Option{ + WithVersion("1.0"), + }, + wantConfig: config{ + version: "1.0", + provider: global.GetLoggerProvider(), + }, + }, + { + name: "with a custom schema URL", + options: []Option{ + WithSchemaURL("https://example.com"), + }, + wantConfig: config{ + schemaURL: "https://example.com", + provider: global.GetLoggerProvider(), + }, + }, + { + name: "with a custom logger provider", + options: []Option{ + WithLoggerProvider(customLoggerProvider), + }, + wantConfig: config{ + provider: customLoggerProvider, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantConfig, newConfig(tt.options)) + }) + } +} + +func TestNewHook(t *testing.T) { + const name = "test_hook" + provider := global.GetLoggerProvider() + + for _, tt := range []struct { + name string + options []Option + wantLogger log.Logger + }{ + { + name: "with default options", + wantLogger: provider.Logger(name), + }, + { + name: "with version and schema URL", + options: []Option{ + WithVersion("1.0"), + WithSchemaURL("https://example.com"), + }, + wantLogger: provider.Logger(name, + log.WithInstrumentationVersion("1.0"), + log.WithSchemaURL("https://example.com"), + ), + }, + } { + t.Run(tt.name, func(t *testing.T) { + hook := NewHook(name, tt.options...) + assert.NotNil(t, hook) + assert.Equal(t, tt.wantLogger, hook.logger) + }) + } +} diff --git a/versions.yaml b/versions.yaml index bc852e73a15..e24308efac4 100644 --- a/versions.yaml +++ b/versions.yaml @@ -88,6 +88,7 @@ module-sets: modules: - go.opentelemetry.io/contrib/detectors/azure/azurevm excluded-modules: + - go.opentelemetry.io/contrib/bridges/otelzerolog - go.opentelemetry.io/contrib/bridges/otelzap - go.opentelemetry.io/contrib/instrgen - go.opentelemetry.io/contrib/instrgen/driver