diff --git a/.drone.yml b/.drone.yml index e41e5b8..fe70c27 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,5 +1,7 @@ +--- kind: pipeline -name: default +type: docker +name: logger steps: - name: test @@ -18,3 +20,28 @@ steps: volumes: - name: deps temp: {} + +--- +kind: pipeline +type: docker +name: otel + +steps: +- name: test + image: golang:1.21.5 + volumes: + - name: deps + path: /go/src/mod + commands: + - cd handler/otel + - go test + +- name: golangci-lint + image: golangci/golangci-lint:v1.55 + commands: + - cd handler/otel + - golangci-lint run + +volumes: +- name: deps + temp: {} diff --git a/.golangci.yml b/.golangci.yml index 1491609..7216484 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,6 @@ +run: + timeout: 5m + linters-settings: dupl: threshold: 100 @@ -52,6 +55,7 @@ linters: - maligned - depguard # need configure + - nolintlint # use with space issues: # Excluding configuration per-path, per-linter, per-text and per-source diff --git a/field/value.go b/field/value.go index 7681786..6ff09f8 100644 --- a/field/value.go +++ b/field/value.go @@ -1,4 +1,4 @@ -//nolint: exhaustruct +// nolint: exhaustruct package field import ( @@ -504,7 +504,7 @@ func (v Value) append(dst []byte) []byte { } } -//nolint: gocyclo,cyclop +// nolint: gocyclo,cyclop func (v Value) Any() any { switch v.Kind { case KindAny, KindBinary: @@ -542,7 +542,7 @@ func (v Value) Any() any { return v.any } -//nolint: forcetypeassert +// nolint: forcetypeassert func (v Value) AsString() string { if v.Kind != KindString { return "" 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) + } +} diff --git a/logger_example_test.go b/logger_example_test.go index 7f21118..36ea6f7 100644 --- a/logger_example_test.go +++ b/logger_example_test.go @@ -92,7 +92,7 @@ var ( float64Val = float64(math.MaxFloat64) minute = time.Minute - timeVal = time.Unix(0, math.MaxInt32) + timeVal = time.Unix(0, math.MaxInt32).In(time.UTC) ) func ExampleNew_anyField() { @@ -108,7 +108,7 @@ func ExampleNew_anyField() { field.Any("error", errors.New("error")), ) // Output: - // {"msg":"any info message","obj":{"Name":"obj name","IsEnable":false},"obj":{"Name":"test obj","IsEnable":false},"int":9223372036854775807,"uint":18446744073709551615,"float":1.7976931348623157e+308,"time":"1970-01-01T03:00:02+03:00","duration":"1h0m0s","error":"error"} + // {"msg":"any info message","obj":{"Name":"obj name","IsEnable":false},"obj":{"Name":"test obj","IsEnable":false},"int":9223372036854775807,"uint":18446744073709551615,"float":1.7976931348623157e+308,"time":"1970-01-01T00:00:02Z","duration":"1h0m0s","error":"error"} } func ExampleNew_arrayField() { @@ -130,11 +130,11 @@ func ExampleNew_arrayField() { field.Complex64s("complex64s", 42, 24), field.Complex128s("complex128s", 42, 24), field.Durations("durations", time.Minute, time.Second), - field.Times("times", time.Unix(0, 42), time.Unix(0, 24)), + field.Times("times", time.Unix(0, 42).In(time.UTC), time.Unix(0, 24).In(time.UTC)), field.Errors("errors", errors.New("error"), errors.New("error2")), ) // Output: - // {"msg":"array info message","strings":["string","test str"],"bools":[true,false],"ints":[42,24],"int8s":[42,24],"int16s":[42,24],"int32s":[42,24],"int64s":[42,24],"uint8s":[255,0],"uint16s":[42,24],"uint32s":[42,24],"uint64s":[42,24],"float32s":[42,24],"float64s":[42,24],"complex64s":["(42+0i)","(24+0i)"],"complex128s":["(42+0i)","(24+0i)"],"durations":["1m0s","1s"],"times":["1970-01-01T03:00:00+03:00","1970-01-01T03:00:00+03:00"],"errors":["error","error2"]} + // {"msg":"array info message","strings":["string","test str"],"bools":[true,false],"ints":[42,24],"int8s":[42,24],"int16s":[42,24],"int32s":[42,24],"int64s":[42,24],"uint8s":[255,0],"uint16s":[42,24],"uint32s":[42,24],"uint64s":[42,24],"float32s":[42,24],"float64s":[42,24],"complex64s":["(42+0i)","(24+0i)"],"complex128s":["(42+0i)","(24+0i)"],"durations":["1m0s","1s"],"times":["1970-01-01T00:00:00Z","1970-01-01T00:00:00Z"],"errors":["error","error2"]} } func ExampleNew_pointerField() { @@ -174,7 +174,7 @@ func ExampleNew_pointerField() { field.Timep("timep", nil), ) // Output: - // {"msg":"pointer info message","stringp":"test str","stringp":null,"boolp":true,"boolp":null,"intp":9223372036854775807,"intp":null,"int8p":127,"int8p":null,"int16p":32767,"int16p":null,"int32p":2147483647,"int32p":null,"int64p":9223372036854775807,"int64p":null,"uintp":18446744073709551615,"uintp":null,"uint8p":255,"uint8p":null,"uint16p":32767,"uint16p":null,"uint32p":2147483647,"uint32p":null,"uint64p":9223372036854775807,"uint64p":null,"float32p":3.4028235e+38,"float32p":null,"float64p":1.7976931348623157e+308,"float64p":null,"durationp":"1m0s","durationp":null,"timep":"1970-01-01T03:00:02+03:00","timep":null} + // {"msg":"pointer info message","stringp":"test str","stringp":null,"boolp":true,"boolp":null,"intp":9223372036854775807,"intp":null,"int8p":127,"int8p":null,"int16p":32767,"int16p":null,"int32p":2147483647,"int32p":null,"int64p":9223372036854775807,"int64p":null,"uintp":18446744073709551615,"uintp":null,"uint8p":255,"uint8p":null,"uint16p":32767,"uint16p":null,"uint32p":2147483647,"uint32p":null,"uint64p":9223372036854775807,"uint64p":null,"float32p":3.4028235e+38,"float32p":null,"float64p":1.7976931348623157e+308,"float64p":null,"durationp":"1m0s","durationp":null,"timep":"1970-01-01T00:00:02Z","timep":null} } func ExampleNew_fields() { @@ -196,12 +196,12 @@ func ExampleNew_fields() { field.Complex64("complex16", 42), field.Complex128("complex128", 42), field.Duration("duration", time.Minute), - field.Time("time", time.Unix(0, 42)), + field.Time("time", timeVal), field.FormatTime("format_time", time.UnixDate, timeVal), field.Error("error", errors.New("error")), ) // Output: - // {"msg":"info message","string":"test str","bool":true,"int":42,"int8":42,"int16":42,"int32":42,"int64":42,"uint8":255,"uint16":42,"uint32":42,"uint64":42,"float32":42,"float64":42,"complex16":"(42+0i)","complex128":"(42+0i)","duration":"1m0s","time":"1970-01-01T03:00:00+03:00","format_time":"Thu Jan 1 03:00:02 MSK 1970","error":"error"} + // {"msg":"info message","string":"test str","bool":true,"int":42,"int8":42,"int16":42,"int32":42,"int64":42,"uint8":255,"uint16":42,"uint32":42,"uint64":42,"float32":42,"float64":42,"complex16":"(42+0i)","complex128":"(42+0i)","duration":"1m0s","time":"1970-01-01T00:00:02Z","format_time":"Thu Jan 1 00:00:02 UTC 1970","error":"error"} } func ExampleNew_jsonFormat() {