-
Notifications
You must be signed in to change notification settings - Fork 582
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
Logging Bridge For ZeroLog #5761
Changes from 2 commits
d3cccee
8904c3c
8dd9248
08984db
963fab0
17b897d
7893d47
abf42b3
5b9f918
9bce58d
b645af8
a1c37ca
fd8d9ba
946d0f8
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 |
---|---|---|
@@ -0,0 +1,23 @@ | ||
module go.opentelemetry.io/contrib/bridges/otelzerolog | ||
|
||
go 1.22.1 | ||
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. This repo currently needs to support Go 1.21. 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. I resolved this in #5782 |
||
|
||
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 | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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= |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package otelzerolog | ||
AkhigbeEromo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import ( | ||
"github.com/rs/zerolog" | ||
"fmt" | ||
"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 | ||
}) | ||
} | ||
func NewHook(name string, options ...Option) *SeverityHook { | ||
cfg := newConfig(options) | ||
return &SeverityHook{ | ||
logger: cfg.logger(name), | ||
} | ||
} | ||
type SeverityHook struct{ | ||
logger log.Logger | ||
levels zerolog.Level | ||
} | ||
|
||
// Levels returns the list of log levels we want to be sent to OpenTelemetry. | ||
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. This doesn't return a list. Is it supposed to return the minimum logged severity? 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. It now returns a list |
||
func (h *SeverityHook) Levels() zerolog.Level { | ||
return h.levels | ||
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. This is never set, it will always be empty. 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. I just set it in the Severity Struct |
||
} | ||
|
||
func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) error { | ||
if level != zerolog.NoLevel { | ||
e.Str("severity", level.String()) | ||
} | ||
h.logger.Emit(e.GetCtx(),h.convertEvent(e,level,msg)) | ||
return nil | ||
} | ||
func(h *SeverityHook) convertEvent(e *zerolog.Event,level zerolog.Level, msg string) log.Record{ | ||
var record log.Record | ||
record.SetTimestamp(zerolog.TimestampFunc()) | ||
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. The observed timestamp should also be set here. 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. I have added it as time.now(), hope that is okay |
||
record.SetBody(log.StringValue(msg)) | ||
const sevOffset = zerolog.Level(log.SeverityDebug) - zerolog.DebugLevel | ||
record.SetSeverity(log.Severity(level + sevOffset)) | ||
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. This assumes the same scaling factor. OTel levels to not scale at this rate: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber |
||
fields := extractFields(e) | ||
|
||
record.AddAttributes(convertFields(fields,msg)...); | ||
return record | ||
} | ||
func extractFields(_ *zerolog.Event) map[string]interface{} { | ||
// Here you would implement the logic to extract fields from the zerolog event | ||
// This might involve using reflection or zerolog internals if necessary | ||
fields := make(map[string]interface{}) | ||
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. This is ... worrying, it is allocating a map just to make a conversion. Can the zero log fields not be directly converted sent to |
||
// Dummy implementation - replace with actual field extraction | ||
|
||
return fields | ||
} | ||
func convertFields(fields map[string]interface{}, msg string) []log.KeyValue { | ||
kvs := make([]log.KeyValue, 0, len(fields)) | ||
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. Similar here, this is making an allocation that is going to be potentially thrown away every time a message is emitted. |
||
kvs = append(kvs, log.String("message", msg)) | ||
for k, v := range fields { | ||
kvs = append(kvs, convertAttribute(k, v)) | ||
} | ||
return kvs | ||
} | ||
|
||
func convertAttribute(key string, value interface{}) log.KeyValue { | ||
switch v := value.(type) { | ||
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. Missing cases:
|
||
case bool: | ||
return log.Bool(key, v) | ||
case []byte: | ||
return log.String(key, string(v)) | ||
case float64: | ||
return log.Float64(key, v) | ||
case int: | ||
return log.Int(key, v) | ||
case int64: | ||
return log.Int64(key, v) | ||
case string: | ||
return log.String(key, v) | ||
default: | ||
// Fallback to string representation for unhandled types | ||
return log.String(key, fmt.Sprintf("%v", v)) | ||
} | ||
} | ||
|
||
|
||
|
||
|
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. There is a fair amount of test coverage missing. If we plan to implement a method or function in the PR it should be accompanied with comprehensive tests. If you need to break this into smaller PRs, adding a method/function stub that you will implement and test in a follow-up PR is acceptable. You will need to track that as an issue though and keep the bridge unreleased until all parts are complete. 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. Thank you @MrAlias 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. I wanted to check in regarding this PR. While I haven't finished all the implementations yet, are the functions I've initialized sufficient, or do I need to add any additional functions? @MrAlias 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. I am more asking about adding test coverage for the functions that are included in this PR. There is a considerable amount of functionality being introduced here that is not tested. 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. Understood |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package otelzerolog | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/rs/zerolog" | ||
"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-logger" | ||
provider := global.GetLoggerProvider() | ||
|
||
for _, tt := range []struct { | ||
name string | ||
options []Option | ||
|
||
wantLogger log.Logger | ||
}{ | ||
{ | ||
name: "with the default options", | ||
wantLogger: provider.Logger(name), | ||
}, | ||
{ | ||
name: "with a 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) | ||
}) | ||
} | ||
} | ||
|
||
func TestSeverityHook_Levels(t *testing.T) { | ||
hook := &SeverityHook{ | ||
levels: zerolog.DebugLevel, | ||
} | ||
|
||
assert.Equal(t, zerolog.DebugLevel, hook.Levels()) | ||
} | ||
|
||
// TODO: Tests for the Run Method |
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.
A changelog entry is required.