diff --git a/log/DESIGN.md b/log/DESIGN.md index df8872ba269..c5277fad0d3 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -43,78 +43,12 @@ type LoggerProvider interface{ ### Logger The [`Logger` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) -is defined as an interface. - -```go -type Logger interface{ - embedded.Logger - Emit(ctx context.Context, record Record) -} -``` +is defined as an interface in [logger.go](logger.go). ### Record The [`LogRecord` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) -is defined as a struct. - -```go -type Record struct { - Timestamp time.Time - ObservedTimestamp time.Time - Severity Severity - SeverityText string - Body string - - // 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 [nAttrsInline]attribute.KeyValue - - // The number of attributes in front. - nFront int - - // The list of attributes except for those in front. - // Invariants: - // - len(back) > 0 iff nFront == len(front) - // - Unused array elements are zero. Used to detect mistakes. - back []attribute.KeyValue -} - -const nAttrsInline = 5 - -type Severity int - -const ( - SeverityUndefined Severity = iota - SeverityTrace - SeverityTrace2 - SeverityTrace3 - SeverityTrace4 - SeverityDebug - SeverityDebug2 - SeverityDebug3 - SeverityDebug4 - SeverityInfo - SeverityInfo2 - SeverityInfo3 - SeverityInfo4 - SeverityWarn - SeverityWarn2 - SeverityWarn3 - SeverityWarn4 - SeverityError - SeverityError2 - SeverityError3 - SeverityError4 - SeverityFatal - SeverityFatal2 - SeverityFatal3 - SeverityFatal4 -) -``` +is defined as a struct in [record.go](record.go). `Record` has `Attributes` and `AddAttributes` methods, like [`slog.Record.Attrs`](https://pkg.go.dev/log/slog#Record.Attrs) @@ -133,9 +67,6 @@ naive implementation. ```go type handler struct { logger log.Logger - level slog.Level - attrs []attribute.KeyValue - prefix string } func (h *handler) Handle(ctx context.Context, r slog.Record) error { @@ -143,8 +74,8 @@ func (h *handler) Handle(ctx context.Context, r slog.Record) error { record := Record{Timestamp: r.Time, Severity: lvl, Body: r.Message} - if r.NumAttrs() > 5 { - attrs := make([]attribute.KeyValue, 0, len(r.NumAttrs())) + if r.AttributesLen() > 5 { + attrs := make([]attribute.KeyValue, 0, len(r.AttributesLen())) r.Attrs(func(a slog.Attr) bool { attrs = append(attrs, convertAttr(a)) return true diff --git a/log/bench_test.go b/log/bench_test.go deleted file mode 100644 index b26765c44c3..00000000000 --- a/log/bench_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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_test - -import "testing" - -// These benchmarks are based on slog/internal/benchmarks. -// They have the following desirable properties: -// -// - They test a complete log record, from the user's call to its return. -// -// - The benchmarked code is run concurrently in multiple goroutines, to -// better simulate a real server (the most common environment for structured -// logs). -// -// - Some handlers are optimistic versions of real handlers, doing real-world -// tasks as fast as possible (and sometimes faster, in that an -// implementation may not be concurrency-safe). This gives us an upper bound -// on handler performance, so we can evaluate the (handler-independent) core -// activity of the package in an end-to-end context without concern that a -// slow handler implementation is skewing the results. -func BenchmarkEndToEnd(b *testing.B) { - // TODO: Replicate https://github.com/golang/go/blob/master/src/log/slog/internal/benchmarks/benchmarks_test.go - // Run benchmarks against a "noop.Logger" and "fastTextLogger" (based on fastTextHandler) -} diff --git a/log/benchmark/bench_test.go b/log/benchmark/bench_test.go new file mode 100644 index 00000000000..aeb50fbb5a2 --- /dev/null +++ b/log/benchmark/bench_test.go @@ -0,0 +1,170 @@ +// 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 benchmark + +import ( + "context" + "io" + "testing" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/noop" +) + +var ( + ctx = context.Background() + testTimestamp = time.Date(1988, time.November, 17, 0, 0, 0, 0, time.UTC) + testBody = "log message" + testSeverity = log.SeverityInfo + testFloat = 1.2345 + testString = "7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190" + testInt = 32768 + testBool = true +) + +// These benchmarks are based on slog/internal/benchmarks. +// +// They test a complete log record, from the user's call to its return. +// +// 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{Timestamp: testTimestamp, Severity: testSeverity, Body: testBody} + tc.logger.Emit(ctx, r) + }, + }, + { + "3 attrs", + func() { + r := log.Record{Timestamp: testTimestamp, Severity: testSeverity, Body: testBody} + r.AddAttributes( + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + ) + tc.logger.Emit(ctx, r) + }, + }, + { + // 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() { + r := log.Record{Timestamp: testTimestamp, Severity: testSeverity, Body: testBody} + r.AddAttributes( + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + ) + tc.logger.Emit(ctx, r) + }, + }, + { + "10 attrs", + func() { + r := log.Record{Timestamp: testTimestamp, Severity: testSeverity, Body: testBody} + r.AddAttributes( + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + ) + tc.logger.Emit(ctx, r) + }, + }, + { + "40 attrs", + func() { + r := log.Record{Timestamp: testTimestamp, Severity: testSeverity, Body: testBody} + r.AddAttributes( + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.Bool("bool", testBool), + attribute.String("string", testString), + ) + tc.logger.Emit(ctx, r) + }, + }, + } { + b.Run(call.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + call.f() + } + }) + } + }) + } +} diff --git a/log/benchmark/impl.go b/log/benchmark/impl.go new file mode 100644 index 00000000000..28430c30470 --- /dev/null +++ b/log/benchmark/impl.go @@ -0,0 +1,67 @@ +// 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 benchmark + +import ( + "context" + "fmt" + "io" + "strconv" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/embedded" +) + +// 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(_ 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(" ") + l.write("body=") + l.write(r.Body) + r.Attributes(func(kv attribute.KeyValue) bool { + l.write(" ") + l.write(string(kv.Key)) + l.write("=") + l.appendValue(kv.Value) + return true + }) + l.write("\n") +} + +func (l *writerLogger) appendValue(v attribute.Value) { + switch v.Type() { + case attribute.STRING: + l.write(v.AsString()) + case attribute.INT64: + l.write(strconv.FormatInt(v.AsInt64(), 10)) // strconv.FormatInt allocates memory. + case attribute.FLOAT64: + l.write(strconv.FormatFloat(v.AsFloat64(), 'g', -1, 64)) // strconv.FormatFloat allocates memory. + case attribute.BOOL: + l.write(strconv.FormatBool(v.AsBool())) + default: + panic(fmt.Sprintf("unhandled attribute type: %s", v.Type())) + } +} + +func (l *writerLogger) write(s string) { + _, _ = io.WriteString(l.w, s) +} diff --git a/log/benchmark/impl_test.go b/log/benchmark/impl_test.go new file mode 100644 index 00000000000..f4c782b044f --- /dev/null +++ b/log/benchmark/impl_test.go @@ -0,0 +1,34 @@ +// 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 benchmark + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log" +) + +func TestWriterLogger(t *testing.T) { + sb := &strings.Builder{} + l := &writerLogger{w: sb} + + r := log.Record{Timestamp: testTimestamp, Severity: testSeverity, Body: testBody} + r.AddAttributes( + attribute.String("string", testString), + attribute.Float64("float", testFloat), + attribute.Int("int", testInt), + attribute.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\n" + assert.Equal(t, want, sb.String()) +} 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 index 5765be32942..57c215cbbb8 100644 --- a/log/go.mod +++ b/log/go.mod @@ -1,3 +1,24 @@ module go.opentelemetry.io/otel/log go 1.20 + +require ( + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.21.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/otel/trace => ../trace + +replace go.opentelemetry.io/otel/metric => ../metric + +replace go.opentelemetry.io/otel => ../ diff --git a/log/go.sum b/log/go.sum new file mode 100644 index 00000000000..130a4f410be --- /dev/null +++ b/log/go.sum @@ -0,0 +1,16 @@ +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/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/logger.go b/log/logger.go new file mode 100644 index 00000000000..f75c9c9c9d0 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,18 @@ +// 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 TODO: comment. +type Logger interface { + embedded.Logger + + // Emit TODO: comment. + 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..c1f78d10440 --- /dev/null +++ b/log/provider.go @@ -0,0 +1,92 @@ +// 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 TODO: comment. +type LoggerProvider interface { + embedded.LoggerProvider + + // Logger TODO: comment. + 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 Meter options. +type LoggerOption interface { + // applyMeter is used to set a LoggerOption value of a LoggerConfig. + applyMeter(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.applyMeter(config) + } + return config +} + +type loggerOptionFunc func(LoggerConfig) LoggerConfig + +func (fn loggerOptionFunc) applyMeter(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/record.go b/log/record.go new file mode 100644 index 00000000000..7ab48cccaa9 --- /dev/null +++ b/log/record.go @@ -0,0 +1,155 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log // import "go.opentelemetry.io/otel/log" + +import ( + "errors" + "slices" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) + +var errUnsafeAddAttrs = errors.New("unsafely called AddAttrs on copy of Record made without using Record.Clone") + +// Record TODO: comment. +// TODO: Add unit tests. +type Record struct { + // TODO: comment. + Timestamp time.Time + + // TODO: comment. + ObservedTimestamp time.Time + + // TODO: comment. + Severity Severity + + // TODO: comment. + SeverityText string + + // TODO: comment. + Body string + + // 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 [nAttrsInline]attribute.KeyValue + + // The number of attributes in front. + nFront int + + // The list of attributes except for those in front. + // Invariants: + // - len(back) > 0 iff nFront == len(front) + // - Unused array elements are zero. Used to detect mistakes. + back []attribute.KeyValue +} + +const nAttrsInline = 5 + +// Severity TODO: comment. +type Severity int + +// TODO: comment. +const ( + SeverityUndefined Severity = iota + SeverityTrace + SeverityTrace2 + SeverityTrace3 + SeverityTrace4 + SeverityDebug + SeverityDebug2 + SeverityDebug3 + SeverityDebug4 + SeverityInfo + SeverityInfo2 + SeverityInfo3 + SeverityInfo4 + SeverityWarn + SeverityWarn2 + SeverityWarn3 + SeverityWarn4 + SeverityError + SeverityError2 + SeverityError3 + SeverityError4 + SeverityFatal + SeverityFatal2 + SeverityFatal3 + SeverityFatal4 +) + +// Attributes calls f on each [attribute.KeyValue] in the [Record]. +// Iteration stops if f returns false. +func (r Record) Attributes(f func(attribute.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]. +// It omits invalid attributes. +func (r *Record) AddAttributes(attrs ...attribute.KeyValue) { + var i int + for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ { + a := attrs[i] + if !a.Valid() { + continue + } + r.front[r.nFront] = a + r.nFront++ + } + // Check if a copy was modified by slicing past the end + // and seeing if the Attr there is non-zero. + if cap(r.back) > len(r.back) { + end := r.back[:len(r.back)+1][len(r.back)] + if end.Valid() { + // Don't panic; copy and muddle through. + r.back = slices.Clip(r.back) + otel.Handle(errUnsafeAddAttrs) + } + } + ne := countInvalidAttrs(attrs[i:]) + r.back = slices.Grow(r.back, len(attrs[i:])-ne) + for _, a := range attrs[i:] { + if a.Valid() { + r.back = append(r.back, a) + } + } +} + +// Clone returns a copy of the record with no shared state. +// The original record and the clone can both be modified +// without interfering with each other. +func (r Record) Clone() Record { + r.back = slices.Clip(r.back) // prevent append from mutating shared array + return r +} + +// AttributesLen returns the number of attributes in the Record. +func (r Record) AttributesLen() int { + return r.nFront + len(r.back) +} + +// countInvalidAttrs returns the number of invalid attributes. +func countInvalidAttrs(as []attribute.KeyValue) int { + n := 0 + for _, a := range as { + if !a.Valid() { + n++ + } + } + return n +}