diff --git a/.drone.yml b/.drone.yml index e41e5b8..34b81ae 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,6 +15,21 @@ steps: commands: - golangci-lint run +- name: test:otel + image: golang:1.21.5 + volumes: + - name: deps + path: /go/src/mod + commands: + - cd handler/otel + - go test + +- name: golangci-lint:otel + image: golangci/golangci-lint:v1.55 + commands: + - cd handler/otel + - golangci-lint run + volumes: - name: deps temp: {} diff --git a/handler/otel/go.mod b/handler/otel/go.mod new file mode 100644 index 0000000..215f2d4 --- /dev/null +++ b/handler/otel/go.mod @@ -0,0 +1,17 @@ +module gitoa.ru/go-4devs/log/handler/otel + +go 1.21.5 + +require ( + gitoa.ru/go-4devs/log v0.5.1 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/sdk v1.21.0 + go.opentelemetry.io/otel/trace v1.21.0 +) + +require ( + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + golang.org/x/sys v0.14.0 // indirect +) diff --git a/handler/otel/go.sum b/handler/otel/go.sum new file mode 100644 index 0000000..55f41ec --- /dev/null +++ b/handler/otel/go.sum @@ -0,0 +1,27 @@ +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.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gitoa.ru/go-4devs/log v0.5.1 h1:rrIyjpUaw8AjDCf7ZuH0HgCRf370O3TV29yrU1xizWM= +gitoa.ru/go-4devs/log v0.5.1/go.mod h1:tREtjEH2cTHl0p3uCVcH9g5tlqtsVNI/tDQVfq53Ty4= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/handler/otel/helpers.go b/handler/otel/helpers.go new file mode 100644 index 0000000..63b4558 --- /dev/null +++ b/handler/otel/helpers.go @@ -0,0 +1,59 @@ +package otel + +import ( + "context" + + "gitoa.ru/go-4devs/log/entry" + "gitoa.ru/go-4devs/log/field" + "gitoa.ru/go-4devs/log/level" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const ( + fieldSeverityNumber = "SeverityNumber" + fieldSeverityText = "SeverityText" + levelFields = 2 +) + +func levels(lvl level.Level) Level { + switch lvl { + case level.Emergency: + return levelError3 + case level.Alert: + return levelFatal + case level.Critical: + return levelError2 + case level.Error: + return levelError + case level.Warning: + return levelWarn + case level.Notice: + return levelInfo2 + case level.Info: + return levelInfo + case level.Debug: + return levelDebug + } + + return 0 +} + +func addEvent(ctx context.Context, data *entry.Entry) { + span := trace.SpanFromContext(ctx) + attrs := make([]attribute.KeyValue, 0, data.Fields().Len()+levelFields) + + lvl := levels(data.Level()) + attrs = append(attrs, + attribute.String(fieldSeverityText, lvl.String()), + attribute.Int(fieldSeverityNumber, int(lvl)), + ) + + data.Fields().Fields(func(f field.Field) bool { + attrs = append(attrs, attribute.String(f.Key, f.Value.String())) + + return true + }) + + span.AddEvent(data.Message(), trace.WithAttributes(attrs...)) +} diff --git a/handler/otel/level.go b/handler/otel/level.go new file mode 100644 index 0000000..7d0f0fd --- /dev/null +++ b/handler/otel/level.go @@ -0,0 +1,16 @@ +package otel + +//go:generate stringer -type=Level -linecomment -output=level_string.go + +type Level int + +const ( + levelDebug Level = 5 // DEBUG + levelInfo Level = 9 // INFO + levelInfo2 Level = 10 // INFO2 + levelWarn Level = 13 // WARN + levelError Level = 17 // ERROR + levelError2 Level = 18 // ERROR2 + levelError3 Level = 19 // ERROR3 + levelFatal Level = 21 // FATAL +) diff --git a/handler/otel/level_string.go b/handler/otel/level_string.go new file mode 100644 index 0000000..0a393c2 --- /dev/null +++ b/handler/otel/level_string.go @@ -0,0 +1,51 @@ +// Code generated by "stringer -type=Level -linecomment -output=level_string.go"; DO NOT EDIT. + +package otel + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[levelDebug-5] + _ = x[levelInfo-9] + _ = x[levelInfo2-10] + _ = x[levelWarn-13] + _ = x[levelError-17] + _ = x[levelError2-18] + _ = x[levelError3-19] + _ = x[levelFatal-21] +} + +const ( + _Level_name_0 = "DEBUG" + _Level_name_1 = "INFOINFO2" + _Level_name_2 = "WARN" + _Level_name_3 = "ERRORERROR2ERROR3" + _Level_name_4 = "FATAL" +) + +var ( + _Level_index_1 = [...]uint8{0, 4, 9} + _Level_index_3 = [...]uint8{0, 5, 11, 17} +) + +func (i Level) String() string { + switch { + case i == 5: + return _Level_name_0 + case 9 <= i && i <= 10: + i -= 9 + return _Level_name_1[_Level_index_1[i]:_Level_index_1[i+1]] + case i == 13: + return _Level_name_2 + case 17 <= i && i <= 19: + i -= 17 + return _Level_name_3[_Level_index_3[i]:_Level_index_3[i+1]] + case i == 21: + return _Level_name_4 + default: + return "Level(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/handler/otel/logger.go b/handler/otel/logger.go new file mode 100644 index 0000000..ba256f1 --- /dev/null +++ b/handler/otel/logger.go @@ -0,0 +1,16 @@ +package otel + +import ( + "context" + + "gitoa.ru/go-4devs/log" + "gitoa.ru/go-4devs/log/entry" +) + +func New() log.Logger { + return func(ctx context.Context, e *entry.Entry) (int, error) { + addEvent(ctx, e) + + return 0, nil + } +} diff --git a/handler/otel/logger_example_test.go b/handler/otel/logger_example_test.go new file mode 100644 index 0000000..ec32869 --- /dev/null +++ b/handler/otel/logger_example_test.go @@ -0,0 +1,62 @@ +package otel_test + +import ( + "context" + "fmt" + "io" + + "gitoa.ru/go-4devs/log" + "gitoa.ru/go-4devs/log/field" + "gitoa.ru/go-4devs/log/handler/otel" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +func ExampleNew_withTrace() { + ctx := context.Background() + logger := log.New(log.WithStdout()).With(otel.Middleware()) + + sctx, span := startSpan(ctx) + + logger.Err(sctx, "log logrus") + logger.ErrKV(sctx, "log logrus kv", field.Int("int", 42)) + logger.ErrKVs(sctx, "log logrus kv sugar", "err", io.EOF) + + span.End() + + // Output: + // msg="log logrus" + // msg="log logrus kv" int=42 + // msg="log logrus kv sugar" err=EOF + // event: log logrus, SeverityText = ERROR, SeverityNumber = 17 + // event: log logrus kv, SeverityText = ERROR, SeverityNumber = 17, int = 42 + // event: log logrus kv sugar, SeverityText = ERROR, SeverityNumber = 17, err = EOF +} + +func startSpan(ctx context.Context) (context.Context, trace.Span) { + tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter{})) + + return tp.Tracer("logger").Start(ctx, "operation") +} + +type exporter struct{} + +func (e exporter) Shutdown(_ context.Context) error { + return nil +} + +func (e exporter) ExportSpans(_ context.Context, spanData []sdktrace.ReadOnlySpan) error { + for _, data := range spanData { + for _, events := range data.Events() { + fmt.Print("event: ", events.Name) + + for _, attr := range events.Attributes { + fmt.Printf(", %v = %v", attr.Key, attr.Value.AsInterface()) + } + + fmt.Print("\n") + } + } + + return nil +} diff --git a/handler/otel/middleware.go b/handler/otel/middleware.go new file mode 100644 index 0000000..7b7be1d --- /dev/null +++ b/handler/otel/middleware.go @@ -0,0 +1,16 @@ +package otel + +import ( + "context" + + "gitoa.ru/go-4devs/log" + "gitoa.ru/go-4devs/log/entry" +) + +func Middleware() log.Middleware { + return func(ctx context.Context, e *entry.Entry, handler log.Logger) (int, error) { + addEvent(ctx, e) + + return handler(ctx, e) + } +}