diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 62541218442..ee59fe0158b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -217,6 +217,24 @@ updates: schedule: interval: weekly day: sunday + - package-ecosystem: gomod + directory: /log + labels: + - dependencies + - go + - Skip Changelog + schedule: + interval: weekly + day: sunday + - package-ecosystem: gomod + directory: /log/internal + labels: + - dependencies + - go + - Skip Changelog + schedule: + interval: weekly + day: sunday - package-ecosystem: gomod directory: /metric labels: diff --git a/log/doc.go b/log/doc.go new file mode 100644 index 00000000000..f9a9ec96a36 --- /dev/null +++ b/log/doc.go @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +/* +Package log defines the OpenTelemetry Bridge API. +It is supposed to be used by a log bridge implementation +that is an adapter between an existing logging library and OpenTelemetry. +Application code should not call this API directly. + +Existing logging libraries generally provide a much richer set of features +than in OpenTelemetry. It is not a goal of OpenTelemetry, to ship +a feature-rich logging library. + +# Bridge Implementations + +The bridge implementation should allow passing a [context.Context] containing +a trace context from the caller to [Logger]'s Emit method. + +A bridge can use [sync.Pool] of attributes for reducing the number of +heap allocations. + +# API Implementations + +This package does not conform to the standard Go versioning policy, all of its +interfaces may have methods added to them without a package major version bump. +This non-standard API evolution could surprise an uninformed implementation +author. They could unknowingly build their implementation in a way that would +result in a runtime panic for their users that update to the new API. + +The API is designed to help inform an instrumentation author about this +non-standard API evolution. It requires them to choose a default behavior for +unimplemented interface methods. There are three behavior choices they can +make: + + - Compilation failure + - Panic + - Default to another implementation + +All interfaces in this API embed a corresponding interface from +[go.opentelemetry.io/otel/log/embedded]. If an author wants the default +behavior of their implementations to be a compilation failure, signaling to +their users they need to update to the latest version of that implementation, +they need to embed the corresponding interface from +[go.opentelemetry.io/otel/log/embedded] in their implementation. For +example, + + import "go.opentelemetry.io/otel/log/embedded" + + type LoggerProvider struct { + embedded.LoggerProvider + // ... + } + +If an author wants the default behavior of their implementations to a panic, +they need to embed the API interface directly. + + import "go.opentelemetry.io/otel/log" + + type LoggerProvider struct { + log.LoggerProvider + // ... + } + +This is not a recommended behavior as it could lead to publishing packages that +contain runtime panics when users update other package that use newer versions +of [go.opentelemetry.io/otel/log]. + +Finally, an author can embed another implementation in theirs. The embedded +implementation will be used for methods not defined by the author. For example, +an author who wants to default to silently dropping the call can use +[go.opentelemetry.io/otel/log/noop]: + + import "go.opentelemetry.io/otel/log/noop" + + type LoggerProvider struct { + noop.LoggerProvider + // ... + } + +It is strongly recommended that authors only embed +[go.opentelemetry.io/otel/log/noop] if they choose this default behavior. +That implementation is the only one OpenTelemetry authors can guarantee will +fully implement all the API interfaces when a user updates their API. +*/ +package log // import "go.opentelemetry.io/otel/log" diff --git a/log/embedded/embedded.go b/log/embedded/embedded.go new file mode 100644 index 00000000000..9993ecf73a8 --- /dev/null +++ b/log/embedded/embedded.go @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package embedded provides interfaces embedded within +// the [OpenTelemetry Logs Bridge API]. +// +// Implementers of the [OpenTelemetry Logs Bridge API] can embed the relevant type +// from this package into their implementation directly. Doing so will result +// in a compilation error for users when the [OpenTelemetry Logs Bridge API] is +// extended (which is something that can happen without a major version bump of +// the API package). +// +// [OpenTelemetry Logs Bridge API]: https://pkg.go.dev/go.opentelemetry.io/otel/log +package embedded // import "go.opentelemetry.io/otel/log/embedded" + +// LoggerProvider is embedded in +// [go.opentelemetry.io/otel/log.LoggerProvider]. +// +// Embed this interface in your implementation of the +// [go.opentelemetry.io/otel/log.LoggerProvider] if you want users to +// experience a compilation error, signaling they need to update to your latest +// implementation, when the [go.opentelemetry.io/otel/log.LoggerProvider] +// interface is extended (which is something that can happen without a major +// version bump of the API package). +type LoggerProvider interface{ loggerProvider() } + +// Logger is embedded in [go.opentelemetry.io/otel/log.Logger]. +// +// Embed this interface in your implementation of the +// [go.opentelemetry.io/otel/log.Logger] if you want users to experience a +// compilation error, signaling they need to update to your latest +// implementation, when the [go.opentelemetry.io/otel/log.Logger] interface +// is extended (which is something that can happen without a major version bump +// of the API package). +type Logger interface{ logger() } diff --git a/log/go.mod b/log/go.mod new file mode 100644 index 00000000000..4def3819908 --- /dev/null +++ b/log/go.mod @@ -0,0 +1,20 @@ +module go.opentelemetry.io/otel/log + +go 1.20 + +require ( + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.23.0-rc.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/otel => ../ + +replace go.opentelemetry.io/otel/trace => ../trace + +replace go.opentelemetry.io/otel/metric => ../metric diff --git a/log/go.sum b/log/go.sum new file mode 100644 index 00000000000..a6bcd03a15e --- /dev/null +++ b/log/go.sum @@ -0,0 +1,11 @@ +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +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= +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/log/internal/bench_test.go b/log/internal/bench_test.go new file mode 100644 index 00000000000..f3fd4a86f67 --- /dev/null +++ b/log/internal/bench_test.go @@ -0,0 +1,415 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// These benchmarks are based on slog/internal/benchmarks. +// +// They test a complete log record, from the user's call to its return. + +package internal + +import ( + "context" + "io" + "testing" + "time" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/noop" + "go.opentelemetry.io/otel/trace" + + "github.com/go-logr/logr" + "golang.org/x/exp/slog" +) + +var ( + ctx = trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{TraceID: [16]byte{1}, SpanID: [8]byte{42}})) + testTimestamp = time.Date(1988, time.November, 17, 0, 0, 0, 0, time.UTC) + testBodyString = "log message" + testBody = log.StringValue(testBodyString) + testSeverity = log.SeverityInfo + testFloat = 1.2345 + testString = "7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190" + testInt = 32768 + testBool = true +) + +// WriterLogger is an optimistic version of a real logger, doing real-world +// tasks as fast as possible . This gives us an upper bound +// on handler performance, so we can evaluate the (logger-independent) core +// activity of the package in an end-to-end context without concern that a +// slow logger implementation is skewing the results. The writerLogger +// allocates memory only when using strconv. +func BenchmarkEmit(b *testing.B) { + for _, tc := range []struct { + name string + logger log.Logger + }{ + {"noop", noop.Logger{}}, + {"writer", &writerLogger{w: io.Discard}}, + } { + b.Run(tc.name, func(b *testing.B) { + for _, call := range []struct { + name string + f func() + }{ + { + "no attrs", + func() { + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + tc.logger.Emit(ctx, r) + }, + }, + { + "3 attrs", + func() { + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + ) + tc.logger.Emit(ctx, r) + }, + }, + { + // The number should match nAttrsInline in record.go and in slog/record.go. + // This should exercise the code path where no allocations + // happen in Record or Attr. If there are allocations, they + // should only be from strconv used in writerLogger. + "5 attrs", + func() { + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + ) + tc.logger.Emit(ctx, r) + }, + }, + { + "10 attrs", + func() { + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + ) + tc.logger.Emit(ctx, r) + }, + }, + { + "40 attrs", + func() { + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + log.String("string", testString), + ) + tc.logger.Emit(ctx, r) + }, + }, + } { + b.Run(call.name, func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + call.f() + } + }) + }) + } + }) + } +} + +func BenchmarkSlog(b *testing.B) { + logger := slog.New(&slogHandler{noop.Logger{}}) + for _, call := range []struct { + name string + f func() + }{ + { + "no attrs", + func() { + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString) + }, + }, + { + "3 attrs", + func() { + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + ) + }, + }, + { + "5 attrs", + func() { + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + ) + }, + }, + { + "10 attrs", + func() { + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + ) + }, + }, + { + "40 attrs", + func() { + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + slog.String("string", testString), + slog.Float64("float", testFloat), + slog.Int("int", testInt), + slog.Bool("bool", testBool), + slog.String("string", testString), + ) + }, + }, + } { + b.Run(call.name, func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + call.f() + } + }) + }) + } +} + +func BenchmarkLogr(b *testing.B) { + logger := logr.New(&logrSink{noop.Logger{}}) + for _, call := range []struct { + name string + f func() + }{ + { + "no attrs", + func() { + logger.Info(testBodyString) + }, + }, + { + "3 attrs", + func() { + logger.Info(testBodyString, + "string", testString, + "float", testFloat, + "int", testInt, + ) + }, + }, + { + // The number should match nAttrsInline in record.go. + // This should exercise the code path where no allocations + // happen in Record or Attr. If there are allocations, they + // should only be from strconv used in writerLogger. + "5 attrs", + func() { + logger.Info(testBodyString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + ) + }, + }, + { + "10 attrs", + func() { + logger.Info(testBodyString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + ) + }, + }, + { + "40 attrs", + func() { + logger.Info(testBodyString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + "string", testString, + "float", testFloat, + "int", testInt, + "bool", testBool, + "string", testString, + ) + }, + }, + } { + b.Run(call.name, func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + call.f() + } + }) + }) + } +} diff --git a/log/internal/go.mod b/log/internal/go.mod new file mode 100644 index 00000000000..44fd9f344dc --- /dev/null +++ b/log/internal/go.mod @@ -0,0 +1,26 @@ +module go.opentelemetry.io/otel/log/internal + +go 1.20 + +require ( + github.com/go-logr/logr v1.4.1 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/otel/trace v1.23.0 + golang.org/x/exp v0.0.0-20231127185646-65229373498e +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel v1.23.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/otel/log => ../ + +replace go.opentelemetry.io/otel => ../.. + +replace go.opentelemetry.io/otel/trace => ../../trace + +replace go.opentelemetry.io/otel/metric => ../../metric diff --git a/log/internal/go.sum b/log/internal/go.sum new file mode 100644 index 00000000000..949da0b69c3 --- /dev/null +++ b/log/internal/go.sum @@ -0,0 +1,15 @@ +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.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +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= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +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/log/internal/logr.go b/log/internal/logr.go new file mode 100644 index 00000000000..43c62da3c11 --- /dev/null +++ b/log/internal/logr.go @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "go.opentelemetry.io/otel/log/internal" + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + + "go.opentelemetry.io/otel/log" +) + +type logrSink struct { + Logger log.Logger +} + +// Init is implemented as a dummy. +func (s *logrSink) Init(info logr.RuntimeInfo) { +} + +// Enabled is implemented as a dummy. +func (s *logrSink) Enabled(level int) bool { + return true +} + +// Info logs a non-error message with the given key/value pairs as context. +// It should avoid memory allocations whenever possible. +func (s *logrSink) Info(level int, msg string, keysAndValues ...any) { + record := log.Record{} + + record.SetBody(log.StringValue(msg)) + + lvl := log.Severity(9 - level) + record.SetSeverity(lvl) + + if len(keysAndValues)%2 == 1 { + panic("key without a value") + } + kvCount := len(keysAndValues) / 2 + ctx := context.Background() + for i := 0; i < kvCount; i++ { + k, ok := keysAndValues[i*2].(string) + if !ok { + panic("key is not a string") + } + v := keysAndValues[i*2+1] + if vCtx, ok := v.(context.Context); ok { + // Special case when a field is of context.Context type. + ctx = vCtx + continue + } + kv := convertKV(k, v) + record.AddAttributes(kv) + } + + s.Logger.Emit(ctx, record) +} + +// Error is implemented as a dummy. +func (s *logrSink) Error(err error, msg string, keysAndValues ...any) { +} + +// WithValues is implemented as a dummy. +func (s *logrSink) WithValues(keysAndValues ...any) logr.LogSink { + return s +} + +// WithName is implemented as a dummy. +func (s *logrSink) WithName(name string) logr.LogSink { + return s +} + +func convertKV(k string, v interface{}) log.KeyValue { + switch val := v.(type) { + case bool: + return log.Bool(k, val) + case float64: + return log.Float64(k, val) + case int: + return log.Int(k, val) + case string: + return log.String(k, val) + default: + panic(fmt.Sprintf("unhandled value type: %T", val)) + } +} diff --git a/log/internal/logr_test.go b/log/internal/logr_test.go new file mode 100644 index 00000000000..ff0aa7999cc --- /dev/null +++ b/log/internal/logr_test.go @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestLogrSink(t *testing.T) { + spy := &spyLogger{} + + l := logr.New(&logrSink{spy}) + + l.Info(testBodyString, "string", testString, "ctx", ctx) + + want := log.Record{} + want.SetBody(testBody) + want.SetSeverity(log.SeverityInfo) + want.AddAttributes(log.String("string", testString)) + + assert.Equal(t, testBody, spy.Record.Body()) + assert.Equal(t, log.SeverityInfo, spy.Record.Severity()) + assert.Equal(t, 1, spy.Record.AttributesLen()) + spy.Record.WalkAttributes(func(kv log.KeyValue) bool { + assert.Equal(t, "string", string(kv.Key)) + assert.Equal(t, testString, kv.Value.AsString()) + return true + }) +} diff --git a/log/internal/slog.go b/log/internal/slog.go new file mode 100644 index 00000000000..b6104e3576b --- /dev/null +++ b/log/internal/slog.go @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "go.opentelemetry.io/otel/log/internal" + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/log" + + "golang.org/x/exp/slog" +) + +type slogHandler struct { + Logger log.Logger +} + +// Handle handles the Record. +// It should avoid memory allocations whenever possible. +func (h *slogHandler) Handle(ctx context.Context, r slog.Record) error { + record := log.Record{} + + record.SetTimestamp(r.Time) + + record.SetBody(log.StringValue(r.Message)) + + lvl := convertLevel(r.Level) + record.SetSeverity(lvl) + + r.Attrs(func(a slog.Attr) bool { + record.AddAttributes(convertAttr(a)) + return true + }) + + h.Logger.Emit(ctx, record) + return nil +} + +// Enabled is implemented as a dummy. +func (h *slogHandler) Enabled(_ context.Context, _ slog.Level) bool { + return true +} + +// WithAttrs is implemented as a dummy. +func (h *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return h +} + +// WithGroup is implemented as a dummy. +func (h *slogHandler) WithGroup(name string) slog.Handler { + return h +} + +func convertLevel(l slog.Level) log.Severity { + return log.Severity(l + 9) +} + +func convertAttr(attr slog.Attr) log.KeyValue { + val := convertValue(attr.Value) + return log.KeyValue{Key: attr.Key, Value: val} +} + +func convertValue(v slog.Value) log.Value { + switch v.Kind() { + case slog.KindAny: + return log.StringValue(fmt.Sprintf("%+v", v.Any())) + case slog.KindBool: + return log.BoolValue(v.Bool()) + case slog.KindDuration: + return log.Int64Value(v.Duration().Nanoseconds()) + case slog.KindFloat64: + return log.Float64Value(v.Float64()) + case slog.KindInt64: + return log.Int64Value(v.Int64()) + case slog.KindString: + return log.StringValue(v.String()) + case slog.KindTime: + return log.Int64Value(v.Time().UnixNano()) + case slog.KindUint64: + return log.Int64Value(int64(v.Uint64())) + default: + panic(fmt.Sprintf("unhandled attribute kind: %s", v.Kind())) + } +} diff --git a/log/internal/slog_test.go b/log/internal/slog_test.go new file mode 100644 index 00000000000..19af0424476 --- /dev/null +++ b/log/internal/slog_test.go @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "testing" + + "go.opentelemetry.io/otel/log" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slog" +) + +func TestSlogHandler(t *testing.T) { + spy := &spyLogger{} + l := slog.New(&slogHandler{spy}) + + l.InfoContext(ctx, testBodyString, "string", testString) + + want := log.Record{} + want.SetBody(testBody) + want.SetSeverity(log.SeverityInfo) + want.AddAttributes(log.String("string", testString)) + + assert.Equal(t, testBody, spy.Record.Body()) + assert.Equal(t, log.SeverityInfo, spy.Record.Severity()) + assert.Equal(t, 1, spy.Record.AttributesLen()) + spy.Record.WalkAttributes(func(kv log.KeyValue) bool { + assert.Equal(t, "string", string(kv.Key)) + assert.Equal(t, testString, kv.Value.AsString()) + return true + }) +} diff --git a/log/internal/spy_test.go b/log/internal/spy_test.go new file mode 100644 index 00000000000..c91682e7b23 --- /dev/null +++ b/log/internal/spy_test.go @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "context" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/embedded" +) + +type spyLogger struct { + embedded.Logger + Context context.Context + Record log.Record +} + +func (l *spyLogger) Emit(ctx context.Context, r log.Record) { + l.Context = ctx + l.Record = r +} diff --git a/log/internal/writer_logger.go b/log/internal/writer_logger.go new file mode 100644 index 00000000000..a8d9f1f70a8 --- /dev/null +++ b/log/internal/writer_logger.go @@ -0,0 +1,79 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "go.opentelemetry.io/otel/log/internal" + +import ( + "context" + "fmt" + "io" + "strconv" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/embedded" + "go.opentelemetry.io/otel/trace" +) + +// writerLogger is a logger that writes to a provided io.Writer without any locking. +// It is intended to represent a high-performance logger that synchronously +// writes text. +type writerLogger struct { + embedded.Logger + w io.Writer +} + +func (l *writerLogger) Emit(ctx context.Context, r log.Record) { + if !r.Timestamp().IsZero() { + l.write("timestamp=") + l.write(strconv.FormatInt(r.Timestamp().Unix(), 10)) + l.write(" ") + } + l.write("severity=") + l.write(strconv.FormatInt(int64(r.Severity()), 10)) + l.write(" ") + + if !r.Body().Empty() { + l.write("body=") + l.appendValue(r.Body()) + } + + r.WalkAttributes(func(kv log.KeyValue) bool { + l.write(" ") + l.write(string(kv.Key)) + l.write("=") + l.appendValue(kv.Value) + return true + }) + + span := trace.SpanContextFromContext(ctx) + if span.IsValid() { + l.write(" traced=true") + } + + l.write("\n") +} + +func (l *writerLogger) appendValue(v log.Value) { + switch v.Kind() { + case log.KindString: + l.write(v.AsString()) + case log.KindInt64: + l.write(strconv.FormatInt(v.AsInt64(), 10)) // strconv.FormatInt allocates memory. + case log.KindFloat64: + l.write(strconv.FormatFloat(v.AsFloat64(), 'g', -1, 64)) // strconv.FormatFloat allocates memory. + case log.KindBool: + l.write(strconv.FormatBool(v.AsBool())) + case log.KindBytes: + l.write(fmt.Sprint(v.AsBytes())) + case log.KindMap: + l.write(fmt.Sprint(v.AsMap())) + case log.KindEmpty: + l.write("") + default: + panic(fmt.Sprintf("unhandled value kind: %s", v.Kind())) + } +} + +func (l *writerLogger) write(s string) { + _, _ = io.WriteString(l.w, s) +} diff --git a/log/internal/writer_logger_test.go b/log/internal/writer_logger_test.go new file mode 100644 index 00000000000..9abcc6ccd1b --- /dev/null +++ b/log/internal/writer_logger_test.go @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestWriterLogger(t *testing.T) { + sb := &strings.Builder{} + l := &writerLogger{w: sb} + + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), + ) + l.Emit(ctx, r) + + want := "timestamp=595728000 severity=9 body=log message string=7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190 float=1.2345 int=32768 bool=true traced=true\n" + assert.Equal(t, want, sb.String()) +} diff --git a/log/kind_string.go b/log/kind_string.go new file mode 100644 index 00000000000..bdfaa18665c --- /dev/null +++ b/log/kind_string.go @@ -0,0 +1,30 @@ +// Code generated by "stringer -type=Kind -trimprefix=Kind"; DO NOT EDIT. + +package log + +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[KindEmpty-0] + _ = x[KindBool-1] + _ = x[KindFloat64-2] + _ = x[KindInt64-3] + _ = x[KindString-4] + _ = x[KindBytes-5] + _ = x[KindSlice-6] + _ = x[KindMap-7] +} + +const _Kind_name = "EmptyBoolFloat64Int64StringBytesSliceMap" + +var _Kind_index = [...]uint8{0, 5, 9, 16, 21, 27, 32, 37, 40} + +func (i Kind) String() string { + if i < 0 || i >= Kind(len(_Kind_index)-1) { + return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] +} diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 00000000000..84333af8672 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "context" + + "go.opentelemetry.io/otel/log/embedded" +) + +// Logger emits log records. +// +// Warning: Methods may be added to this interface in minor releases. See +// package documentation on API implementation for information on how to set +// default behavior for unimplemented methods. +type Logger interface { + // Users of the interface can ignore this. This embedded type is only used + // by implementations of this interface. See the "API Implementations" + // section of the package documentation for more information. + embedded.Logger + + // Emit emits a log record. + // The caller must not subsequently mutate the record. + // + // This method should: + // - be safe to call concurrently, + // - ignore the cancellation of the context, + // - handle the trace context passed via context, + // - use the current time as observed timestamp if it is a zero value. + Emit(ctx context.Context, record Record) +} diff --git a/log/noop/noop.go b/log/noop/noop.go new file mode 100644 index 00000000000..2d6e4dd2ebc --- /dev/null +++ b/log/noop/noop.go @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package noop provides an implementation of the [OpenTelemetry Logs Bridge API] that +// produces no telemetry and minimizes used computation resources. +// +// Using this package to implement the [OpenTelemetry Logs Bridge API] will effectively +// disable OpenTelemetry. +// +// This implementation can be embedded in other implementations of the +// [OpenTelemetry Logs Bridge API]. Doing so will mean the implementation defaults to +// no operation for methods it does not implement. +// +// [OpenTelemetry Logs Bridge API]: https://pkg.go.dev/go.opentelemetry.io/otel/log +package noop // import "go.opentelemetry.io/otel/log/noop" + +import ( + "context" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/embedded" +) + +var ( + // Compile-time check this implements the OpenTelemetry API. + _ log.LoggerProvider = LoggerProvider{} + _ log.Logger = Logger{} +) + +// LoggerProvider is an OpenTelemetry No-Op LoggerProvider. +type LoggerProvider struct{ embedded.LoggerProvider } + +// NewLoggerProvider returns a LoggerProvider that does not record any telemetry. +func NewLoggerProvider() LoggerProvider { + return LoggerProvider{} +} + +// Logger returns an OpenTelemetry Logger that does not record any telemetry. +func (LoggerProvider) Logger(string, ...log.LoggerOption) log.Logger { + return Logger{} +} + +// Logger is an OpenTelemetry No-Op Logger. +type Logger struct{ embedded.Logger } + +// Emit does nothing. +func (Logger) Emit(context.Context, log.Record) {} diff --git a/log/provider.go b/log/provider.go new file mode 100644 index 00000000000..ebb43e46ec2 --- /dev/null +++ b/log/provider.go @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log/embedded" +) + +// LoggerProvider provides access to named [Logger] instances. +// +// Warning: Methods may be added to this interface in minor releases. See +// package documentation on API implementation for information on how to set +// default behavior for unimplemented methods. +type LoggerProvider interface { + // Users of the interface can ignore this. This embedded type is only used + // by implementations of this interface. See the "API Implementations" + // section of the package documentation for more information. + embedded.LoggerProvider + + // Logger returns a new [Logger] with the provided name and configuration. + // + // This method should: + // - be safe to call concurrently, + // - use some default name if the passed name is empty. + Logger(name string, options ...LoggerOption) Logger +} + +// LoggerConfig contains options for Logger. +type LoggerConfig struct { + instrumentationVersion string + schemaURL string + attrs attribute.Set + + // Ensure forward compatibility by explicitly making this not comparable. + noCmp [0]func() //nolint: unused // This is indeed used. +} + +// InstrumentationVersion returns the version of the library providing +// instrumentation. +func (cfg LoggerConfig) InstrumentationVersion() string { + return cfg.instrumentationVersion +} + +// InstrumentationAttributes returns the attributes associated with the library +// providing instrumentation. +func (cfg LoggerConfig) InstrumentationAttributes() attribute.Set { + return cfg.attrs +} + +// SchemaURL is the schema_url of the library providing instrumentation. +func (cfg LoggerConfig) SchemaURL() string { + return cfg.schemaURL +} + +// LoggerOption is an interface for applying Logger options. +type LoggerOption interface { + // applyLogger is used to set a LoggerOption value of a LoggerConfig. + applyLogger(LoggerConfig) LoggerConfig +} + +// NewLoggerConfig creates a new LoggerConfig and applies +// all the given options. +func NewLoggerConfig(opts ...LoggerOption) LoggerConfig { + var config LoggerConfig + for _, o := range opts { + config = o.applyLogger(config) + } + return config +} + +type loggerOptionFunc func(LoggerConfig) LoggerConfig + +func (fn loggerOptionFunc) applyLogger(cfg LoggerConfig) LoggerConfig { + return fn(cfg) +} + +// WithInstrumentationVersion sets the instrumentation version. +func WithInstrumentationVersion(version string) LoggerOption { + return loggerOptionFunc(func(config LoggerConfig) LoggerConfig { + config.instrumentationVersion = version + return config + }) +} + +// WithInstrumentationAttributes sets the instrumentation attributes. +// +// The passed attributes will be de-duplicated. +func WithInstrumentationAttributes(attr ...attribute.KeyValue) LoggerOption { + return loggerOptionFunc(func(config LoggerConfig) LoggerConfig { + config.attrs = attribute.NewSet(attr...) + return config + }) +} + +// WithSchemaURL sets the schema URL. +func WithSchemaURL(schemaURL string) LoggerOption { + return loggerOptionFunc(func(config LoggerConfig) LoggerConfig { + config.schemaURL = schemaURL + return config + }) +} diff --git a/log/provider_test.go b/log/provider_test.go new file mode 100644 index 00000000000..23f2ab64be0 --- /dev/null +++ b/log/provider_test.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log" +) + +func TestNewLoggerConfig(t *testing.T) { + version := "v1.1.1" + schemaURL := "https://opentelemetry.io/schemas/1.0.0" + attr := attribute.NewSet( + attribute.String("user", "alice"), + attribute.Bool("admin", true), + ) + + c := log.NewLoggerConfig( + log.WithInstrumentationVersion(version), + log.WithSchemaURL(schemaURL), + log.WithInstrumentationAttributes(attr.ToSlice()...), + ) + + assert.Equal(t, version, c.InstrumentationVersion(), "instrumentation version") + assert.Equal(t, schemaURL, c.SchemaURL(), "schema URL") + assert.Equal(t, attr, c.InstrumentationAttributes(), "instrumentation attributes") +} diff --git a/log/record.go b/log/record.go new file mode 100644 index 00000000000..5439c5ac9a7 --- /dev/null +++ b/log/record.go @@ -0,0 +1,131 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "time" +) + +// Record represents a log record. +type Record struct { + timestamp time.Time + observedTimestamp time.Time + severity Severity + severityText string + body Value + + // The fields below are for optimizing the implementation of + // Attributes and AddAttributes. + + // Allocation optimization: an inline array sized to hold + // the majority of log calls (based on examination of open-source + // code). It holds the start of the list of attributes. + front [attributesInlineCount]KeyValue + + // The number of attributes in front. + nFront int + + // The list of attributes except for those in front. + // Invariants: + // - len(back) > 0 if nFront == len(front) + // - Unused array elements are zero. Used to detect mistakes. + back []KeyValue +} + +const attributesInlineCount = 5 + +// Timestamp returns the time when the log record occurred. +func (r *Record) Timestamp() time.Time { + return r.timestamp +} + +// SetTimestamp sets the time when the log record occurred. +func (r *Record) SetTimestamp(t time.Time) { + r.timestamp = t +} + +// ObservedTimestamp returns the time when the log record was observed. +// If unset the implementation should set it equal to the current time. +func (r *Record) ObservedTimestamp() time.Time { + return r.observedTimestamp +} + +// SetObservedTimestamp sets the time when the log record was observed. +// If unset the implementation should set it equal to the current time. +func (r *Record) SetObservedTimestamp(t time.Time) { + r.observedTimestamp = t +} + +// Severity returns the [Severity] of the log record. +func (r *Record) Severity() Severity { + return r.severity +} + +// SetSeverity sets the [Severity] of the log record. +// Use the values defined as constants. +func (r *Record) SetSeverity(s Severity) { + r.severity = s +} + +// SeverityText returns severity (also known as log level) text. +// This is the original string representation of the severity +// as it is known at the source. +func (r *Record) SeverityText() string { + return r.severityText +} + +// SetSeverityText sets severity (also known as log level) text. +// This is the original string representation of the severity +// as it is known at the source. +func (r *Record) SetSeverityText(s string) { + r.severityText = s +} + +// Body returns the the body of the log record as a strucutured value. +func (r *Record) Body() Value { + return r.body +} + +// SetBody sets the the body of the log record as a strucutured value. +func (r *Record) SetBody(v Value) { + r.body = v +} + +// WalkAttributes calls f on each [KeyValue] in the [Record]. +// Iteration stops if f returns false. +func (r *Record) WalkAttributes(f func(KeyValue) bool) { + for i := 0; i < r.nFront; i++ { + if !f(r.front[i]) { + return + } + } + for _, a := range r.back { + if !f(a) { + return + } + } +} + +// AddAttributes appends the given [attribute.KeyValue] to the [Record]'s +// list of [attribute.KeyValue]. +func (r *Record) AddAttributes(attrs ...KeyValue) { + var i int + for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ { + a := attrs[i] + r.front[r.nFront] = a + r.nFront++ + } + + r.back = sliceGrow(r.back, len(attrs[i:])) + r.back = append(r.back, attrs[i:]...) +} + +// AttributesLen returns the number of attributes in the Record. +func (r *Record) AttributesLen() int { + return r.nFront + len(r.back) +} diff --git a/log/record_test.go b/log/record_test.go new file mode 100644 index 00000000000..6f5300f7460 --- /dev/null +++ b/log/record_test.go @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + testTime = time.Date(1988, time.November, 17, 0, 0, 0, 0, time.UTC) + testSeverity = SeverityInfo + testString = "message" + testFloat = 1.2345 + testInt = 32768 + testBool = true +) + +func TestRecordTimestamp(t *testing.T) { + r := Record{} + + r.SetTimestamp(testTime) + + assert.Equal(t, testTime, r.Timestamp()) +} + +func TestRecordObservedTimestamp(t *testing.T) { + r := Record{} + + r.SetObservedTimestamp(testTime) + + assert.Equal(t, testTime, r.ObservedTimestamp()) +} + +func TestRecordSeverity(t *testing.T) { + r := Record{} + + r.SetSeverity(testSeverity) + + assert.Equal(t, testSeverity, r.Severity()) +} + +func TestRecordSeverityText(t *testing.T) { + r := Record{} + + r.SetSeverityText(testString) + + assert.Equal(t, testString, r.SeverityText()) +} + +func TestRecordBody(t *testing.T) { + r := Record{} + body := StringValue(testString) + + r.SetBody(body) + + assert.Equal(t, body, r.Body()) +} + +func TestRecordAttributes(t *testing.T) { + r := Record{} + attrs := []KeyValue{ + String("k1", testString), + Float64("k2", testFloat), + Int("k3", testInt), + Bool("k4", testBool), + String("k5", testString), + Float64("k6", testFloat), + Int("k7", testInt), + Bool("k8", testBool), + {}, + } + r.AddAttributes(attrs...) + + assert.Equal(t, len(attrs), r.AttributesLen()) + + var got []KeyValue + r.WalkAttributes(func(kv KeyValue) bool { + got = append(got, kv) + return true + }) + assert.Equal(t, attrs, got) + + testCases := []struct { + name string + index int + }{ + { + name: "front", + index: 2, + }, + { + name: "back", + index: 6, + }, + } + for _, tc := range testCases { + i := 0 + r.WalkAttributes(func(kv KeyValue) bool { + i++ + return i < tc.index + }) + assert.Equal(t, tc.index, i, "WalkAttributes early return for %s", tc.name) + } +} diff --git a/log/severity.go b/log/severity.go new file mode 100644 index 00000000000..f411cfee4b4 --- /dev/null +++ b/log/severity.go @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:generate stringer -type=Severity -linecomment + +package log // import "go.opentelemetry.io/otel/log" + +// Severity represents a log record severity (also known as log level). +// Smaller numerical values correspond to less severe log records (such as debug events), +// larger numerical values correspond to more severe log records (such as errors and critical events). +type Severity int + +// Severity values defined by OpenTelemetry. +const ( + // A fine-grained debugging log record. Typically disabled in default configurations. + SeverityTrace1 Severity = 1 // TRACE + SeverityTrace2 Severity = 2 // TRACE2 + SeverityTrace3 Severity = 3 // TRACE3 + SeverityTrace4 Severity = 4 // TRACE4 + + // A debugging log record. + SeverityDebug1 Severity = 5 // DEBUG + SeverityDebug2 Severity = 6 // DEBUG2 + SeverityDebug3 Severity = 7 // DEBUG3 + SeverityDebug4 Severity = 8 // DEBUG4 + + // An informational log record. Indicates that an event happened. + SeverityInfo1 Severity = 9 // INFO + SeverityInfo2 Severity = 10 // INFO2 + SeverityInfo3 Severity = 11 // INFO3 + SeverityInfo4 Severity = 12 // INFO4 + + // A warning log record. Not an error but is likely more important than an informational event. + SeverityWarn1 Severity = 13 // WARN + SeverityWarn2 Severity = 14 // WARN2 + SeverityWarn3 Severity = 15 // WARN3 + SeverityWarn4 Severity = 16 // WARN4 + + // An error log record. Something went wrong. + SeverityError1 Severity = 17 // ERROR + SeverityError2 Severity = 18 // ERROR2 + SeverityError3 Severity = 19 // ERROR3 + SeverityError4 Severity = 20 // ERROR4 + + // A fatal log record such as application or system crash. + SeverityFatal1 Severity = 21 // FATAL + SeverityFatal2 Severity = 22 // FATAL2 + SeverityFatal3 Severity = 23 // FATAL3 + SeverityFatal4 Severity = 24 // FATAL4 + + SeverityTrace = SeverityTrace1 + SeverityDebug = SeverityDebug1 + SeverityInfo = SeverityInfo1 + SeverityWarn = SeverityWarn1 + SeverityError = SeverityError1 + SeverityFatal = SeverityFatal1 +) diff --git a/log/severity_string.go b/log/severity_string.go new file mode 100644 index 00000000000..d742ae5fe88 --- /dev/null +++ b/log/severity_string.go @@ -0,0 +1,47 @@ +// Code generated by "stringer -type=Severity -linecomment"; DO NOT EDIT. + +package log + +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[SeverityTrace1-1] + _ = x[SeverityTrace2-2] + _ = x[SeverityTrace3-3] + _ = x[SeverityTrace4-4] + _ = x[SeverityDebug1-5] + _ = x[SeverityDebug2-6] + _ = x[SeverityDebug3-7] + _ = x[SeverityDebug4-8] + _ = x[SeverityInfo1-9] + _ = x[SeverityInfo2-10] + _ = x[SeverityInfo3-11] + _ = x[SeverityInfo4-12] + _ = x[SeverityWarn1-13] + _ = x[SeverityWarn2-14] + _ = x[SeverityWarn3-15] + _ = x[SeverityWarn4-16] + _ = x[SeverityError1-17] + _ = x[SeverityError2-18] + _ = x[SeverityError3-19] + _ = x[SeverityError4-20] + _ = x[SeverityFatal1-21] + _ = x[SeverityFatal2-22] + _ = x[SeverityFatal3-23] + _ = x[SeverityFatal4-24] +} + +const _Severity_name = "TRACETRACE2TRACE3TRACE4DEBUGDEBUG2DEBUG3DEBUG4INFOINFO2INFO3INFO4WARNWARN2WARN3WARN4ERRORERROR2ERROR3ERROR4FATALFATAL2FATAL3FATAL4" + +var _Severity_index = [...]uint8{0, 5, 11, 17, 23, 28, 34, 40, 46, 50, 55, 60, 65, 69, 74, 79, 84, 89, 95, 101, 107, 112, 118, 124, 130} + +func (i Severity) String() string { + i -= 1 + if i < 0 || i >= Severity(len(_Severity_index)-1) { + return "Severity(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _Severity_name[_Severity_index[i]:_Severity_index[i+1]] +} diff --git a/log/severity_test.go b/log/severity_test.go new file mode 100644 index 00000000000..076befb7bca --- /dev/null +++ b/log/severity_test.go @@ -0,0 +1,208 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestSeverity(t *testing.T) { + testCases := []struct { + name string + severity log.Severity + value int + str string + }{ + { + name: "SeverityTrace", + severity: log.SeverityTrace, + value: 1, + str: "TRACE", + }, + { + name: "SeverityTrace1", + severity: log.SeverityTrace1, + value: 1, + str: "TRACE", + }, + { + name: "SeverityTrace2", + severity: log.SeverityTrace2, + value: 2, + str: "TRACE2", + }, + { + name: "SeverityTrace3", + severity: log.SeverityTrace3, + value: 3, + str: "TRACE3", + }, + { + name: "SeverityTrace4", + severity: log.SeverityTrace4, + value: 4, + str: "TRACE4", + }, + { + name: "SeverityDebug", + severity: log.SeverityDebug, + value: 5, + str: "DEBUG", + }, + { + name: "SeverityDebug1", + severity: log.SeverityDebug1, + value: 5, + str: "DEBUG", + }, + { + name: "SeverityDebug2", + severity: log.SeverityDebug2, + value: 6, + str: "DEBUG2", + }, + { + name: "SeverityDebug3", + severity: log.SeverityDebug3, + value: 7, + str: "DEBUG3", + }, + { + name: "SeverityDebug4", + severity: log.SeverityDebug4, + value: 8, + str: "DEBUG4", + }, + { + name: "SeverityInfo", + severity: log.SeverityInfo, + value: 9, + str: "INFO", + }, + { + name: "SeverityInfo1", + severity: log.SeverityInfo1, + value: 9, + str: "INFO", + }, + { + name: "SeverityInfo2", + severity: log.SeverityInfo2, + value: 10, + str: "INFO2", + }, + { + name: "SeverityInfo3", + severity: log.SeverityInfo3, + value: 11, + str: "INFO3", + }, + { + name: "SeverityInfo4", + severity: log.SeverityInfo4, + value: 12, + str: "INFO4", + }, + { + name: "SeverityWarn", + severity: log.SeverityWarn, + value: 13, + str: "WARN", + }, + { + name: "SeverityWarn1", + severity: log.SeverityWarn1, + value: 13, + str: "WARN", + }, + { + name: "SeverityWarn2", + severity: log.SeverityWarn2, + value: 14, + str: "WARN2", + }, + { + name: "SeverityWarn3", + severity: log.SeverityWarn3, + value: 15, + str: "WARN3", + }, + { + name: "SeverityWarn4", + severity: log.SeverityWarn4, + value: 16, + str: "WARN4", + }, + { + name: "SeverityError", + severity: log.SeverityError, + value: 17, + str: "ERROR", + }, + { + name: "SeverityError1", + severity: log.SeverityError1, + value: 17, + str: "ERROR", + }, + { + name: "SeverityError2", + severity: log.SeverityError2, + value: 18, + str: "ERROR2", + }, + { + name: "SeverityError3", + severity: log.SeverityError3, + value: 19, + str: "ERROR3", + }, + { + name: "SeverityError4", + severity: log.SeverityError4, + value: 20, + str: "ERROR4", + }, + { + name: "SeverityFatal", + severity: log.SeverityFatal, + value: 21, + str: "FATAL", + }, + { + name: "SeverityFatal1", + severity: log.SeverityFatal1, + value: 21, + str: "FATAL", + }, + { + name: "SeverityFatal2", + severity: log.SeverityFatal2, + value: 22, + str: "FATAL2", + }, + { + name: "SeverityFatal3", + severity: log.SeverityFatal3, + value: 23, + str: "FATAL3", + }, + { + name: "SeverityFatal4", + severity: log.SeverityFatal4, + value: 24, + str: "FATAL4", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.value, int(tc.severity)) + assert.Equal(t, tc.str, tc.severity.String()) + }) + } +} diff --git a/log/slice.go b/log/slice.go new file mode 100644 index 00000000000..376a2c87936 --- /dev/null +++ b/log/slice.go @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package log // import "go.opentelemetry.io/otel/log" + +// sliceGrow increases the slice's capacity, if necessary, to guarantee space +// for another n elements. After Grow(n), at least n elements can be appended +// to the slice without another allocation. If n is negative or too large to +// allocate the memory, Grow panics. +// +// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. +func sliceGrow[S ~[]E, E any](s S, n int) S { + if n -= cap(s) - len(s); n > 0 { + s = append(s[:cap(s)], make([]E, n)...)[:len(s)] + } + return s +} + +// sliceEqualFunc reports whether two slices are equal using an equality +// function on each pair of elements. If the lengths are different, +// EqualFunc returns false. Otherwise, the elements are compared in +// increasing index order, and the comparison stops at the first index +// for which eq returns false. +// +// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. +func sliceEqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool { + if len(s1) != len(s2) { + return false + } + for i, v1 := range s1 { + v2 := s2[i] + if !eq(v1, v2) { + return false + } + } + return true +} diff --git a/log/value.go b/log/value.go new file mode 100644 index 00000000000..33012ae0be5 --- /dev/null +++ b/log/value.go @@ -0,0 +1,364 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate stringer -type=Kind -trimprefix=Kind + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "bytes" + "errors" + "fmt" + "math" + "strconv" + "unsafe" + + "go.opentelemetry.io/otel/internal/global" +) + +// A Value can represent a structured value. +// The zero Value corresponds to nil. +type Value struct { + _ [0]func() // disallow == + // num holds the value for Kinds: Int64, Float64, and Bool, + // the length for String, Bytes, List, Map. + num uint64 + // If any is of type Kind, then the value is in num as described above. + // Otherwise (if is of type stringptr, bytesptr, sliceptr or mapptr) it contains the value. + any any +} + +type ( + stringptr *byte // used in Value.any when the Value is a string + bytesptr *byte // used in Value.any when the Value is a []byte + sliceptr *Value // used in Value.any when the Value is a []Value + mapptr *KeyValue // used in Value.any when the Value is a []KeyValue +) + +// Kind is the kind of a [Value]. +type Kind int + +// Kind values. +const ( + KindEmpty Kind = iota + KindBool + KindFloat64 + KindInt64 + KindString + KindBytes + KindSlice + KindMap +) + +var emptyString = "" + +// Kind returns v's Kind. +func (v Value) Kind() Kind { + switch x := v.any.(type) { + case Kind: + return x + case stringptr: + return KindString + case bytesptr: + return KindBytes + case sliceptr: + return KindSlice + case mapptr: + return KindMap + default: + return KindEmpty + } +} + +var errBadKind = errors.New("bad kind") + +// StringValue returns a new [Value] for a string. +func StringValue(value string) Value { + return Value{num: uint64(len(value)), any: stringptr(unsafe.StringData(value))} +} + +// IntValue returns a [Value] for an int. +func IntValue(v int) Value { + return Int64Value(int64(v)) +} + +// Int64Value returns a [Value] for an int64. +func Int64Value(v int64) Value { + return Value{num: uint64(v), any: KindInt64} +} + +// Float64Value returns a [Value] for a floating-point number. +func Float64Value(v float64) Value { + return Value{num: math.Float64bits(v), any: KindFloat64} +} + +// BoolValue returns a [Value] for a bool. +func BoolValue(v bool) Value { //nolint:revive // We are passing bool as this is a constructor for bool. + u := uint64(0) + if v { + u = 1 + } + return Value{num: u, any: KindBool} +} + +// BytesValue returns a [Value] for bytes. +// The caller must not subsequently mutate the argument slice. +func BytesValue(v []byte) Value { + return Value{num: uint64(len(v)), any: bytesptr(unsafe.SliceData(v))} +} + +// SliceValue returns a [Value] for a slice of [Value]. +// The caller must not subsequently mutate the argument slice. +func SliceValue(vs ...Value) Value { + return Value{num: uint64(len(vs)), any: sliceptr(unsafe.SliceData(vs))} +} + +// MapValue returns a new [Value] for a slice of key-value pairs. +// The caller must not subsequently mutate the argument slice. +func MapValue(kvs ...KeyValue) Value { + return Value{num: uint64(len(kvs)), any: mapptr(unsafe.SliceData(kvs))} +} + +// AsAny returns v's value as an any. +// It returns nil and logs error if v's kind is invalid. +func (v Value) AsAny() any { + switch v.Kind() { + case KindMap: + return v.mapValue() + case KindSlice: + return v.list() + case KindInt64: + return int64(v.num) + case KindFloat64: + return v.float() + case KindString: + return v.str() + case KindBool: + return v.bool() + case KindBytes: + return v.bytes() + case KindEmpty: + return nil + default: + global.Error(errBadKind, "AsAny", "kind", v.Kind()) + return nil + } +} + +// AsString returns Value's value as a string. +// It returns empty string and logs error if v is not a string. +func (v Value) AsString() string { + if sp, ok := v.any.(stringptr); ok { + return unsafe.String(sp, v.num) + } + global.Error(errBadKind, "AsString", "kind", v.Kind()) + return "" +} + +func (v Value) str() string { + return unsafe.String(v.any.(stringptr), v.num) +} + +// AsInt64 returns v's value as an int64. +// It returns 0 and logs error if v is not a signed integer. +func (v Value) AsInt64() int64 { + if g, w := v.Kind(), KindInt64; g != w { + global.Error(errBadKind, "AsInt64", "kind", v.Kind()) + return 0 + } + return int64(v.num) +} + +// AsBool returns v's value as a bool. +// It returns false and logs error if v is not a bool. +func (v Value) AsBool() bool { + if g, w := v.Kind(), KindBool; g != w { + global.Error(errBadKind, "AsBool", "kind", v.Kind()) + return false + } + return v.bool() +} + +func (v Value) bool() bool { + return v.num == 1 +} + +// AsFloat64 returns v's value as a float64. +// It returns false and logs error if v is not a float64. +func (v Value) AsFloat64() float64 { + if g, w := v.Kind(), KindFloat64; g != w { + global.Error(errBadKind, "AsFloat64", "kind", v.Kind()) + return 0 + } + + return v.float() +} + +func (v Value) float() float64 { + return math.Float64frombits(v.num) +} + +// AsBytes returns v's value as a []byte. +// It returns nil and logs error if v's [Kind] is not [KindBytes]. +func (v Value) AsBytes() []byte { + if sp, ok := v.any.(bytesptr); ok { + return unsafe.Slice((*byte)(sp), v.num) + } + global.Error(errBadKind, "AsBytes", "kind", v.Kind()) + return nil +} + +func (v Value) bytes() []byte { + return unsafe.Slice((*byte)(v.any.(bytesptr)), v.num) +} + +// AsSlice returns v's value as a []Value. +// It returns nil and logs error if v's [Kind] is not [KindSlice]. +func (v Value) AsSlice() []Value { + if sp, ok := v.any.(sliceptr); ok { + return unsafe.Slice((*Value)(sp), v.num) + } + global.Error(errBadKind, "AsSlice", "kind", v.Kind()) + return nil +} + +func (v Value) list() []Value { + return unsafe.Slice((*Value)(v.any.(sliceptr)), v.num) +} + +// AsMap returns v's value as a []KeyValue. +// It returns nil and logs error if v's [Kind] is not [KindMap]. +func (v Value) AsMap() []KeyValue { + if sp, ok := v.any.(mapptr); ok { + return unsafe.Slice((*KeyValue)(sp), v.num) + } + global.Error(errBadKind, "AsMap", "kind", v.Kind()) + return nil +} + +func (v Value) mapValue() []KeyValue { + return unsafe.Slice((*KeyValue)(v.any.(mapptr)), v.num) +} + +// Empty reports whether the value is empty (coresponds to nil). +func (v Value) Empty() bool { + return v.Kind() == KindEmpty +} + +// Equal reports whether v and w represent the same Go value. +func (v Value) Equal(w Value) bool { + k1 := v.Kind() + k2 := w.Kind() + if k1 != k2 { + return false + } + switch k1 { + case KindInt64, KindBool: + return v.num == w.num + case KindString: + return v.str() == w.str() + case KindFloat64: + return v.float() == w.float() + case KindSlice: + return sliceEqualFunc(v.list(), w.list(), Value.Equal) + case KindMap: + return sliceEqualFunc(v.mapValue(), w.mapValue(), KeyValue.Equal) + case KindBytes: + return bytes.Equal(v.bytes(), w.bytes()) + case KindEmpty: + return true + default: + panic(fmt.Sprintf("bad kind: %s", k1)) + } +} + +// String returns Value's value as a string, formatted like [fmt.Sprint]. +func (v Value) String() string { + switch v.Kind() { + case KindString: + return v.str() + case KindInt64: + return strconv.FormatInt(int64(v.num), 10) + case KindFloat64: + return strconv.FormatFloat(v.float(), 'g', -1, 64) + case KindBool: + return strconv.FormatBool(v.bool()) + case KindBytes: + return fmt.Sprint(v.bytes()) + case KindMap: + return fmt.Sprint(v.mapValue()) + case KindSlice: + return fmt.Sprint(v.list()) + case KindEmpty: + return emptyString + default: + return "" + } +} + +// An KeyValue is a key-value pair. +// It is used to represent a log attribute, +// which is a superset of [go.opentelemetry.io/otel/attribute.KeyValue], +// and map item. +type KeyValue struct { + Key string + Value Value +} + +// String returns an KeyValue for a string value. +func String(key, value string) KeyValue { + return KeyValue{key, StringValue(value)} +} + +// Int64 returns an KeyValue for an int64. +func Int64(key string, value int64) KeyValue { + return KeyValue{key, Int64Value(value)} +} + +// Int converts an int to an int64 and returns +// an KeyValue with that value. +func Int(key string, value int) KeyValue { + return Int64(key, int64(value)) +} + +// Float64 returns an KeyValue for a floating-point number. +func Float64(key string, v float64) KeyValue { + return KeyValue{key, Float64Value(v)} +} + +// Bool returns an KeyValue for a bool. +func Bool(key string, v bool) KeyValue { + return KeyValue{key, BoolValue(v)} +} + +// Bytes returns an KeyValue for a bytes. +func Bytes(key string, v []byte) KeyValue { + return KeyValue{key, BytesValue(v)} +} + +// Slice returns an KeyValue for a slice of [Value]. +func Slice(key string, args ...Value) KeyValue { + return KeyValue{key, SliceValue(args...)} +} + +// Map returns an KeyValue for a Map [Value]. +// +// Use Map to collect several key-value pairs under a single +// key. +func Map(key string, args ...KeyValue) KeyValue { + return KeyValue{key, MapValue(args...)} +} + +// Equal reports whether a and b have equal keys and values. +func (a KeyValue) Equal(b KeyValue) bool { + return a.Key == b.Key && a.Value.Equal(b.Value) +} + +// String returns key-value pair as a string, formatted like "key=value". +func (a KeyValue) String() string { + return fmt.Sprintf("%s=%s", a.Key, a.Value) +} diff --git a/log/value_test.go b/log/value_test.go new file mode 100644 index 00000000000..e936bd80f4e --- /dev/null +++ b/log/value_test.go @@ -0,0 +1,209 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package log + +import ( + "fmt" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +func TestKind(t *testing.T) { + testCases := []struct { + kind Kind + str string + value int + }{ + {KindBool, "Bool", 1}, + {KindBytes, "Bytes", 5}, + {KindEmpty, "Empty", 0}, + {KindFloat64, "Float64", 2}, + {KindInt64, "Int64", 3}, + {KindSlice, "Slice", 6}, + {KindMap, "Map", 7}, + {KindString, "String", 4}, + } + for _, tc := range testCases { + t.Run(tc.str, func(t *testing.T) { + assert.Equal(t, tc.value, int(tc.kind)) + assert.Equal(t, tc.str, tc.kind.String()) + }) + } +} + +func TestValueEqual(t *testing.T) { + vals := []Value{ + {}, + Int64Value(1), + Int64Value(2), + Float64Value(3.5), + Float64Value(3.7), + BoolValue(true), + BoolValue(false), + StringValue("hi"), + BytesValue([]byte{1, 3, 5}), + SliceValue(IntValue(3), StringValue("foo")), + MapValue(Bool("b", true), Int("i", 3)), + MapValue(Slice("l", IntValue(3), StringValue("foo")), Bytes("b", []byte{3, 5, 7})), + } + for i, v1 := range vals { + for j, v2 := range vals { + got := v1.Equal(v2) + want := i == j + if got != want { + t.Errorf("%v.Equal(%v): got %t, want %t", v1, v2, got, want) + } + } + } +} + +func TestValueString(t *testing.T) { + for _, test := range []struct { + v Value + want string + }{ + {Int64Value(-3), "-3"}, + {Float64Value(.15), "0.15"}, + {BoolValue(true), "true"}, + {StringValue("foo"), "foo"}, + {BytesValue([]byte{2, 4, 6}), "[2 4 6]"}, + {SliceValue(IntValue(3), StringValue("foo")), "[3 foo]"}, + {MapValue(Int("a", 1), Bool("b", true)), "[a=1 b=true]"}, + {Value{}, ""}, + } { + got := test.v.String() + assert.Equal(t, test.want, got) + } +} + +func TestValueNoAlloc(t *testing.T) { + // Assign values just to make sure the compiler doesn't optimize away the statements. + var ( + i int64 + f float64 + b bool + by []byte + s string + ) + bytes := []byte{1, 3, 4} + a := int(testing.AllocsPerRun(5, func() { + i = Int64Value(1).AsInt64() + f = Float64Value(1).AsFloat64() + b = BoolValue(true).AsBool() + by = BytesValue(bytes).AsBytes() + s = StringValue("foo").AsString() + })) + assert.Zero(t, a) + _ = i + _ = f + _ = b + _ = by + _ = s +} + +func TestKeyValueNoAlloc(t *testing.T) { + // Assign values just to make sure the compiler doesn't optimize away the statements. + var ( + i int64 + f float64 + b bool + by []byte + s string + ) + bytes := []byte{1, 3, 4} + a := int(testing.AllocsPerRun(5, func() { + i = Int64("key", 1).Value.AsInt64() + f = Float64("key", 1).Value.AsFloat64() + b = Bool("key", true).Value.AsBool() + by = Bytes("key", bytes).Value.AsBytes() + s = String("key", "foo").Value.AsString() + })) + assert.Zero(t, a) + _ = i + _ = f + _ = b + _ = by + _ = s +} + +func TestValueAny(t *testing.T) { + for _, test := range []struct { + want any + in Value + }{ + {"s", StringValue("s")}, + {true, BoolValue(true)}, + {int64(4), IntValue(4)}, + {int64(11), Int64Value(11)}, + {1.5, Float64Value(1.5)}, + {[]byte{1, 2, 3}, BytesValue([]byte{1, 2, 3})}, + {[]Value{IntValue(3)}, SliceValue(IntValue(3))}, + {[]KeyValue{Int("i", 3)}, MapValue(Int("i", 3))}, + {nil, Value{}}, + } { + got := test.in.AsAny() + assert.Equal(t, test.want, got) + } +} + +func TestEmptyMap(t *testing.T) { + g := Map("g") + got := g.Value.AsMap() + assert.Nil(t, got) +} + +func TestEmptyList(t *testing.T) { + l := SliceValue() + got := l.AsSlice() + assert.Nil(t, got) +} + +func TestMapValueWithEmptyMaps(t *testing.T) { + // Preserve empty groups. + g := MapValue( + Int("a", 1), + Map("g1", Map("g2")), + Map("g3", Map("g4", Int("b", 2)))) + got := g.AsMap() + want := []KeyValue{Int("a", 1), Map("g1", Map("g2")), Map("g3", Map("g4", Int("b", 2)))} + assert.Equal(t, want, got) +} + +func TestListValueWithEmptyValues(t *testing.T) { + // Preserve empty values. + l := SliceValue(Value{}) + got := l.AsSlice() + want := []Value{{}} + assert.Equal(t, want, got) +} + +// A Value with "unsafe" strings is significantly faster: +// safe: 1785 ns/op, 0 allocs +// unsafe: 690 ns/op, 0 allocs + +// Run this with and without -tags unsafe_kvs to compare. +func BenchmarkUnsafeStrings(b *testing.B) { + b.ReportAllocs() + dst := make([]Value, 100) + src := make([]Value, len(dst)) + b.Logf("Value size = %d", unsafe.Sizeof(Value{})) + for i := range src { + src[i] = StringValue(fmt.Sprintf("string#%d", i)) + } + b.ResetTimer() + var d string + for i := 0; i < b.N; i++ { + copy(dst, src) + for _, a := range dst { + d = a.AsString() + } + } + _ = d +} diff --git a/versions.yaml b/versions.yaml index 028393f078d..4c01488f8b1 100644 --- a/versions.yaml +++ b/versions.yaml @@ -50,3 +50,5 @@ module-sets: - go.opentelemetry.io/otel/schema excluded-modules: - go.opentelemetry.io/otel/internal/tools + - go.opentelemetry.io/otel/log + - go.opentelemetry.io/otel/log/internal