Skip to content
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

logrus logging bridge #5356

Merged
merged 39 commits into from May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4466582
initialize otellogrus package
dmathieu Apr 2, 2024
a0c4702
add otellogrus to dependabot
dmathieu Apr 2, 2024
6ef4b09
add otellogrus to codeowners
dmathieu Apr 2, 2024
f5034e8
start setting up fire
dmathieu Apr 2, 2024
a4493f4
add fields
dmathieu Apr 2, 2024
a31b4c8
convert key values
dmathieu Apr 3, 2024
9b52b71
add some more comment
dmathieu Apr 3, 2024
f447e39
fix lint
dmathieu Apr 3, 2024
d2ebd7a
document attributes transformations
dmathieu Apr 3, 2024
85aedd8
merge config.go into hook.go
dmathieu Apr 3, 2024
54b6603
fix package name
dmathieu Apr 3, 2024
055fd2d
use oteltest recorder
dmathieu Apr 10, 2024
4d332ba
rename expected to want
dmathieu Apr 10, 2024
850bfc5
handle struct, slices, maps and pointers
dmathieu Apr 10, 2024
02c2b85
add changelog entry
dmathieu Apr 10, 2024
591c08a
run go mod tidy
dmathieu Apr 10, 2024
76171c8
fix lint
dmathieu Apr 10, 2024
0ca93fa
temporarily fix multiple attributes order issue
dmathieu Apr 10, 2024
3bcee7b
add package to versions.yaml
dmathieu Apr 12, 2024
6a9a961
simplify appending in convertFields
dmathieu Apr 24, 2024
b076874
don't rely on testify to compare structs
dmathieu Apr 24, 2024
554d348
write tests for the config
dmathieu Apr 24, 2024
7e8af87
add example test
dmathieu Apr 24, 2024
4f04095
add benchmark test
dmathieu Apr 24, 2024
724cc97
fix linter in example_test
dmathieu Apr 24, 2024
f348923
add data to the benchmarked record
dmathieu Apr 24, 2024
febcf43
remove redundant nil check
dmathieu Apr 25, 2024
d6a4cc7
test nested maps
dmathieu Apr 25, 2024
bd04baa
test schema URL in NewHook
dmathieu Apr 25, 2024
43dc19a
use otel 1.26.0
dmathieu May 7, 2024
09573f5
make comment more generic
dmathieu May 7, 2024
b196d68
test interface slices/maps and struct maps
dmathieu May 7, 2024
cd9d821
Update bridges/otellogrus/hook.go
dmathieu May 7, 2024
3bfd477
mention that the body is transformed into a string as well
dmathieu May 7, 2024
0c6c205
change map order for ci
dmathieu May 7, 2024
18a9ba0
Update hook.go
pellared May 10, 2024
f905c84
use the latest log api
dmathieu May 15, 2024
40d61d9
add pellared to code owners
dmathieu May 15, 2024
0edd773
Update CHANGELOG.md
pellared May 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Expand Up @@ -100,6 +100,15 @@ updates:
schedule:
interval: weekly
day: sunday
- package-ecosystem: gomod
directory: /bridges/otellogrus
labels:
- dependencies
- go
- Skip Changelog
schedule:
interval: weekly
day: sunday
- package-ecosystem: gomod
directory: /bridges/otelslog
labels:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -17,6 +17,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The `go.opentelemetry.io/contrib/processors/baggage/baggagetrace` module. This module provides a Baggage Span Processor. (#5404)
- Add gRPC trace `Filter` for stats handler to `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`. (#5196)
- Add a repository Code Ownership Policy. (#5555)
- The `go.opentelemetry.io/contrib/bridges/otellogrus` module.
This module provides an OpenTelemetry logging bridge for `github.com/sirupsen/logrus`. (#5355)

### Changed

Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -23,6 +23,7 @@
CODEOWNERS @MrAlias @MadVikingGod @pellared @dashpole

bridges/otelslog @open-telemetry/go-approvers @MrAlias @pellared
bridges/otellogrus/ @open-telemetry/go-approvers @dmathieu @pellared
bridges/prometheus/ @open-telemetry/go-approvers @dashpole
bridges/otelzap/ @open-telemetry/go-approvers @pellared @khushijain21

Expand Down
22 changes: 22 additions & 0 deletions bridges/otellogrus/example_test.go
@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package otellogrus_test

import (
"github.com/sirupsen/logrus"

"go.opentelemetry.io/contrib/bridges/otellogrus"
"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 an *otellogrus.Hook and use it in your application.
hook := otellogrus.NewHook(otellogrus.WithLoggerProvider(provider))

// Set the newly created hook as a global logrus hook
logrus.AddHook(hook)
}
22 changes: 22 additions & 0 deletions bridges/otellogrus/go.mod
@@ -0,0 +1,22 @@
module go.opentelemetry.io/contrib/bridges/otellogrus

go 1.21

require (
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel/log v0.2.0-alpha.0.20240515103629-7708ace91199
go.opentelemetry.io/otel/sdk v1.24.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/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
golang.org/x/sys v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
36 changes: 36 additions & 0 deletions bridges/otellogrus/go.sum
@@ -0,0 +1,36 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/log v0.2.0-alpha.0.20240515103629-7708ace91199 h1:jHKW4lMJY0fGbONUyJAEdQ+Vc/oY516uGqtCJUtNmpU=
go.opentelemetry.io/otel/log v0.2.0-alpha.0.20240515103629-7708ace91199/go.mod h1:vbFZc65yq4c4ssvXY43y/nIqkNJLxORrqw0L85P59LA=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
241 changes: 241 additions & 0 deletions bridges/otellogrus/hook.go
@@ -0,0 +1,241 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// Package otellogrus provides a [Hook], a [logrus.Hook] implementation that
// can be used to bridge between the [github.com/sirupsen/logrus] API and
// [OpenTelemetry].
//
// # Record Conversion
//
// The [logrus.Entry] records are converted to OpenTelemetry [log.Record] in
// the following way:
//
// - Time is set as the Timestamp.
// - Message is set as the Body using a [log.StringValue].
// - Level is transformed and set as the Severity. The SeverityText is not
// set.
// - Fields are transformed and set as the attributes.
//
// The Level is transformed by using the static offset to the OpenTelemetry
// Severity types. For example:
//
// - [slog.LevelDebug] is transformed to [log.SeverityDebug]
// - [slog.LevelInfo] is transformed to [log.SeverityInfo]
// - [slog.LevelWarn] is transformed to [log.SeverityWarn]
// - [slog.LevelError] is transformed to [log.SeverityError]
//
// Attribute values are transformed based on their type into log attributes, or
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
// into a string value if there is no matching type.
//
// [OpenTelemetry]: https://opentelemetry.io/docs/concepts/signals/logs/
package otellogrus // import "go.opentelemetry.io/contrib/bridges/otellogrus"

import (
"fmt"
"reflect"

"github.com/sirupsen/logrus"

"go.opentelemetry.io/otel/log"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/sdk/instrumentation"
)

const bridgeName = "go.opentelemetry.io/contrib/bridges/otellogrus"

type config struct {
provider log.LoggerProvider
scope instrumentation.Scope

levels []logrus.Level
}

func newConfig(options []Option) config {
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
var c config
for _, opt := range options {
c = opt.apply(c)
}

var emptyScope instrumentation.Scope
if c.scope == emptyScope {
c.scope = instrumentation.Scope{
Name: bridgeName,
Version: version,
}
}

if c.provider == nil {
c.provider = global.GetLoggerProvider()
}

if c.levels == nil {
c.levels = logrus.AllLevels
}

return c
}

func (c config) logger() log.Logger {
var opts []log.LoggerOption
if c.scope.Version != "" {
opts = append(opts, log.WithInstrumentationVersion(c.scope.Version))
}
if c.scope.SchemaURL != "" {
opts = append(opts, log.WithSchemaURL(c.scope.SchemaURL))
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
}
return c.provider.Logger(c.scope.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) }

// WithInstrumentationScope returns an [Option] that configures the scope of
// the logs that will be emitted by the configured [Hook].
//
// By default if this Option is not provided, the Hook will use a default
// instrumentation scope describing this bridge package. It is recommended to
// provide this so log data can be associated with its source package or
// module.
func WithInstrumentationScope(scope instrumentation.Scope) Option {
return optFunc(func(c config) config {
c.scope = scope
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
})
}

// WithLevels returns an [Option] that configures the log levels that will fire
// the configured [Hook].
//
// By default if this Option is not provided, the Hook will fire for all levels.
// LoggerProvider.
func WithLevels(l []logrus.Level) Option {
return optFunc(func(c config) config {
c.levels = l
return c
})
}

// NewHook returns a new [Hook] to be used as a [logrus.Hook].
//
// If [WithLoggerProvider] is not provided, the returned Hook will use the
// global LoggerProvider.
func NewHook(options ...Option) *Hook {
cfg := newConfig(options)
return &Hook{
logger: cfg.logger(),
levels: cfg.levels,
}
}

// Hook is a [logrus.Hook] that sends all logging records it receives to
// OpenTelemetry. See package documentation for how conversions are made.
type Hook struct {
logger log.Logger
levels []logrus.Level
}

// Levels returns the list of log levels we want to be sent to OpenTelemetry.
func (h *Hook) Levels() []logrus.Level {
return h.levels
}

// Fire handles the passed record, and sends it to OpenTelemetry.
func (h *Hook) Fire(entry *logrus.Entry) error {
ctx := entry.Context
h.logger.Emit(ctx, h.convertEntry(entry))
return nil
}

func (h *Hook) convertEntry(e *logrus.Entry) log.Record {
var record log.Record
record.SetTimestamp(e.Time)
record.SetBody(log.StringValue(e.Message))

const sevOffset = logrus.Level(log.SeverityDebug) - logrus.DebugLevel
record.SetSeverity(log.Severity(e.Level + sevOffset))
record.AddAttributes(convertFields(e.Data)...)

return record
}

func convertFields(fields logrus.Fields) []log.KeyValue {
kvs := make([]log.KeyValue, 0, len(fields))
for k, v := range fields {
kvs = append(kvs, log.KeyValue{
Key: k,
Value: convertValue(v),
})
}
return kvs
}

func convertValue(v interface{}) log.Value {
switch v := v.(type) {
case bool:
return log.BoolValue(v)
case []byte:
return log.BytesValue(v)
case float64:
return log.Float64Value(v)
case int:
return log.IntValue(v)
case int64:
return log.Int64Value(v)
case string:
return log.StringValue(v)
}

t := reflect.TypeOf(v)
if t == nil {
return log.Value{}
}
val := reflect.ValueOf(v)
switch t.Kind() {
case reflect.Struct:
return log.StringValue(fmt.Sprintf("%+v", v))
case reflect.Slice, reflect.Array:
items := make([]log.Value, 0, val.Len())
for i := 0; i < val.Len(); i++ {
items = append(items, convertValue(val.Index(i).Interface()))
}
return log.SliceValue(items...)
case reflect.Map:
kvs := make([]log.KeyValue, 0, val.Len())
for _, k := range val.MapKeys() {
var key string
// If the key is a struct, use %+v to print the struct fields.
if k.Kind() == reflect.Struct {
key = fmt.Sprintf("%+v", k.Interface())
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
} else {
key = fmt.Sprintf("%v", k.Interface())
}
kvs = append(kvs, log.KeyValue{
Key: key,
Value: convertValue(val.MapIndex(k).Interface()),
})
}
return log.MapValue(kvs...)
case reflect.Ptr, reflect.Interface:
return convertValue(val.Elem().Interface())
}

return log.StringValue(fmt.Sprintf("unhandled attribute type: (%s) %+v", t, v))

Check warning on line 240 in bridges/otellogrus/hook.go

View check run for this annotation

Codecov / codecov/patch

bridges/otellogrus/hook.go#L240

Added line #L240 was not covered by tests
}