From a6f19fe0f12d514d2923d01330097ff5ae81103f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 17 Nov 2023 10:07:37 +0100 Subject: [PATCH 01/83] Add empty design doc --- docs/design/logs.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/design/logs.md diff --git a/docs/design/logs.md b/docs/design/logs.md new file mode 100644 index 00000000000..491b58cd3b6 --- /dev/null +++ b/docs/design/logs.md @@ -0,0 +1,35 @@ +# Design + +Author: Robert Pająk + +Tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/issues/4696). + +## Abstract + +[A short summary of the proposal.] + +## Background + +[An introduction of the necessary background and the problem being solved by the proposed change.] + +## Proposal + +[A precise statement of the proposed change.] + +## Rationale + +[A discussion of alternate approaches and the trade offs, advantages, and disadvantages of the specified approach.] + +## Compatibility + +[A discussion of the change with regard to the +[compatibility guidelines](https://go.dev/doc/go1compat).] + +## Implementation + +[A description of the steps in the implementation, who will do them, and when.] + +## Open issues (if applicable) + +[A discussion of issues relating to this proposal for which the author does not +know the solution. This section may be omitted if there are none.] From c8fcf28dfea143204e32809c616194b3d701eefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 17 Nov 2023 11:15:09 +0100 Subject: [PATCH 02/83] Init Logs Bridge API design --- .github/dependabot.yml | 9 ++++++++ docs/design/log-api.md | 51 ++++++++++++++++++++++++++++++++++++++++++ docs/design/logs.md | 35 ----------------------------- log/bench_test.go | 30 +++++++++++++++++++++++++ log/doc.go | 5 +++++ log/example_test.go | 8 +++++++ log/go.mod | 3 +++ versions.yaml | 1 + 8 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 docs/design/log-api.md delete mode 100644 docs/design/logs.md create mode 100644 log/bench_test.go create mode 100644 log/doc.go create mode 100644 log/example_test.go create mode 100644 log/go.mod diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 62541218442..aa56688077f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -217,6 +217,15 @@ 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: /metric labels: diff --git a/docs/design/log-api.md b/docs/design/log-api.md new file mode 100644 index 00000000000..9a8ef8b1053 --- /dev/null +++ b/docs/design/log-api.md @@ -0,0 +1,51 @@ +# Logs Bridge API Design + +Author: Robert Pająk + +Tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/issues/4696). + +## Abstract + + + +We propose adding a `go.opentelemetry.io/otel/log` Go module which will provide +[Logs Data Model](https://opentelemetry.io/docs/specs/otel/logs/data-model/) +and [Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/data-model/). + +## Background + + + +They key challenge is to create API which will be complaint with the specification +and be as performant as possible. + +Performance is seen as one of the most imporatant charactristics of logging libraries in Go. + +## Proposal + +The design and benchmarks takes inspiration from [`slog`](https://pkg.go.dev/log/slog), +because for the Go team it was also critical to create API that would be fast +and interoperable with existing logging packages. [^1] [^2] + + + +## Rationale + + + +## Compatibility + +The backwards compatibility is achieved using the `embedded` design pattern +that is already used in Trace API and Metrics API. + +## Implementation + + + +## Open issues (if applicable) + + + +[^1]: Jonathan Amsterdam, [The Go Blog: Structured Logging with slog](https://go.dev/blog/slog) +[^2]: Jonathan Amsterdam, [GopherCon Europe 2023: A Fast Structured Logging Package](https://www.youtube.com/watch?v=tC4Jt3i62ns) diff --git a/docs/design/logs.md b/docs/design/logs.md deleted file mode 100644 index 491b58cd3b6..00000000000 --- a/docs/design/logs.md +++ /dev/null @@ -1,35 +0,0 @@ -# Design - -Author: Robert Pająk - -Tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/issues/4696). - -## Abstract - -[A short summary of the proposal.] - -## Background - -[An introduction of the necessary background and the problem being solved by the proposed change.] - -## Proposal - -[A precise statement of the proposed change.] - -## Rationale - -[A discussion of alternate approaches and the trade offs, advantages, and disadvantages of the specified approach.] - -## Compatibility - -[A discussion of the change with regard to the -[compatibility guidelines](https://go.dev/doc/go1compat).] - -## Implementation - -[A description of the steps in the implementation, who will do them, and when.] - -## Open issues (if applicable) - -[A discussion of issues relating to this proposal for which the author does not -know the solution. This section may be omitted if there are none.] diff --git a/log/bench_test.go b/log/bench_test.go new file mode 100644 index 00000000000..b26765c44c3 --- /dev/null +++ b/log/bench_test.go @@ -0,0 +1,30 @@ +// 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/doc.go b/log/doc.go new file mode 100644 index 00000000000..ad5db29756d --- /dev/null +++ b/log/doc.go @@ -0,0 +1,5 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package log provides the OpenTelemetry Logs Data Model and Bridge API. +package log // import "go.opentelemetry.io/otel/log" diff --git a/log/example_test.go b/log/example_test.go new file mode 100644 index 00000000000..209f5da17d3 --- /dev/null +++ b/log/example_test.go @@ -0,0 +1,8 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log_test + +func Example() { + // Output: +} diff --git a/log/go.mod b/log/go.mod new file mode 100644 index 00000000000..5765be32942 --- /dev/null +++ b/log/go.mod @@ -0,0 +1,3 @@ +module go.opentelemetry.io/otel/log + +go 1.20 diff --git a/versions.yaml b/versions.yaml index 3c153c9d6fc..01eb0123136 100644 --- a/versions.yaml +++ b/versions.yaml @@ -50,3 +50,4 @@ module-sets: - go.opentelemetry.io/otel/schema excluded-modules: - go.opentelemetry.io/otel/internal/tools + - go.opentelemetry.io/otel/log From e48ae14cbdf3046c60a25a4dfca2fcc80a7730b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 17 Nov 2023 12:59:52 +0100 Subject: [PATCH 03/83] Document the design --- docs/design/log-api.md | 127 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 15 deletions(-) diff --git a/docs/design/log-api.md b/docs/design/log-api.md index 9a8ef8b1053..720a6b27a41 100644 --- a/docs/design/log-api.md +++ b/docs/design/log-api.md @@ -1,4 +1,4 @@ -# Logs Bridge API Design +# Logs Bridge API Author: Robert Pająk @@ -9,35 +9,132 @@ Tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/iss We propose adding a `go.opentelemetry.io/otel/log` Go module which will provide -[Logs Data Model](https://opentelemetry.io/docs/specs/otel/logs/data-model/) -and [Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/data-model/). +[Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/). ## Background -They key challenge is to create API which will be complaint with the specification -and be as performant as possible. - +They key challenge is to create a well-performant API compliant with the specification. Performance is seen as one of the most imporatant charactristics of logging libraries in Go. -## Proposal +## Design + +This proposed design aims to: + +- be specification compliant, +- have similar API to Trace and Metrics API, +- take advantage of both OpenTelemetry and `slog` experience to achieve acceptable performance. + +### LoggerProvider + +The [`LoggerProvider` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#loggerprovider) +is defined as an interface. + +```go +type LoggerProvider interface{ + Logger(name string, options ...LoggerOption) Logger +} +``` + +### Logger + +The [`Logger` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) +is defined as an interface. + +```go +type Logger interface{ + Emit(ctx context.Context, options ...RecordOption) +} +``` + +The `Logger` has `Emit(context.Context, options ...RecordOption` method. + +### 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 + + // 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 Attrs. + front [nAttrsInline]attribute.KeyValue + + // The number of Attrs in front. + nFront int + + // The list of Attrs 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 +) +``` + +`Record` has `Attributes` and `AddAttributes` methods, +like [`slog.Record.Attrs`](https://pkg.go.dev/log/slog#Record.Attrs) +and [`slog.Record.AddAttrs`](https://pkg.go.dev/log/slog#Record.AddAttrs), +in order to achieve high-performance when accessing and setting attributes efficiently. + +The `NewRecord(...RecordOption) Record` is a factory function used to create records using provided options. + +`Record` has a `Clone` method to allow copying records so that the SDK can offer concurrency safety. + +## Compatibility + +The backwards compatibility is achieved using the `embedded` design pattern +that is already used in Trace API and Metrics API. + +## Benchmarking -The design and benchmarks takes inspiration from [`slog`](https://pkg.go.dev/log/slog), +The benchmarks takes inspiration from [`slog`](https://pkg.go.dev/log/slog), because for the Go team it was also critical to create API that would be fast and interoperable with existing logging packages. [^1] [^2] - - ## Rationale -## Compatibility - -The backwards compatibility is achieved using the `embedded` design pattern -that is already used in Trace API and Metrics API. - ## Implementation From cd39d631c95844027fc64fd4c6e0ef95101990ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 15:38:03 +0100 Subject: [PATCH 04/83] Add Rationale --- docs/design/log-api.md | 61 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/design/log-api.md b/docs/design/log-api.md index 720a6b27a41..f52fd2a7bf5 100644 --- a/docs/design/log-api.md +++ b/docs/design/log-api.md @@ -26,6 +26,14 @@ This proposed design aims to: - have similar API to Trace and Metrics API, - take advantage of both OpenTelemetry and `slog` experience to achieve acceptable performance. +### Module structure + +The Go module consits of the following packages: + +- `go.opentelemetry.io/otel/log` +- `go.opentelemetry.io/otel/log/embedded` +- `go.opentelemetry.io/otel/log/noop` + ### LoggerProvider The [`LoggerProvider` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#loggerprovider) @@ -33,6 +41,7 @@ is defined as an interface. ```go type LoggerProvider interface{ + embedded.LoggerProvider Logger(name string, options ...LoggerOption) Logger } ``` @@ -44,6 +53,7 @@ is defined as an interface. ```go type Logger interface{ + embedded.Logger Emit(ctx context.Context, options ...RecordOption) } ``` @@ -116,9 +126,11 @@ like [`slog.Record.Attrs`](https://pkg.go.dev/log/slog#Record.Attrs) and [`slog.Record.AddAttrs`](https://pkg.go.dev/log/slog#Record.AddAttrs), in order to achieve high-performance when accessing and setting attributes efficiently. -The `NewRecord(...RecordOption) Record` is a factory function used to create records using provided options. +The `NewRecord(...RecordOption) (Record, error)` is a factory function +used to create records using provided options. -`Record` has a `Clone` method to allow copying records so that the SDK can offer concurrency safety. +`Record` has a `Clone` method to allow copying records +so that the SDK can offer concurrency safety. ## Compatibility @@ -129,15 +141,54 @@ that is already used in Trace API and Metrics API. The benchmarks takes inspiration from [`slog`](https://pkg.go.dev/log/slog), because for the Go team it was also critical to create API that would be fast -and interoperable with existing logging packages. [^1] [^2] +and interoperable with existing logging packages.[^1] ## Rationale -## Implementation +### Logger.Emit definition + +One of the ideas was to have: + +```go +type Logger interface{ + Emit(ctx context.Context, record Record) +} +``` + +This gives the advantage that the SDK would not need to call `NewRecord(options...)`. + +The user can still easily create a helper that could be easier to use: + +```go +func log(ctx context.Context, l Logger, options ...RecordOption) { + r := log.NewRecord(options...) + l.Emit(ctx, r) +} +``` + +The main reasons against this defintion are that following: + +1. The existing design is similar to the [Meter API](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Meter) +for creating instruments. +2. It is unsure if anyone would like to reuse a record. +3. Just passing options should be more-user friendly API. + +### Record as struct + +`Record` is defined as a `struct` because of the following reasons. + +Log record is a value object without any behavior. +It is used as data input for Logger methods. + +The log record resembles the instrument config structs like [metric.Float64CounterConfig](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Float64CounterConfig). + +Using `struct` instead of `interface` should have better the performance as e.g. +indirect calls are less optimized, +usage of intefaces tend to increase heap allocations.[^2] - +The `Record` design is inspired by [`slog.Record`](https://pkg.go.dev/log/slog#Record). ## Open issues (if applicable) From 4578694dd5cd8daaec216f0ed1258d1213b45c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 15:38:43 +0100 Subject: [PATCH 05/83] Move design doc closer to the code --- docs/design/log-api.md => log/DESIGN.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/design/log-api.md => log/DESIGN.md (100%) diff --git a/docs/design/log-api.md b/log/DESIGN.md similarity index 100% rename from docs/design/log-api.md rename to log/DESIGN.md From fbd656fbd24d5f69c9af79c51ddeab874d2a8514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 15:39:34 +0100 Subject: [PATCH 06/83] Cleanup --- log/DESIGN.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index f52fd2a7bf5..eca4dbdb658 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -6,15 +6,11 @@ Tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/iss ## Abstract - - We propose adding a `go.opentelemetry.io/otel/log` Go module which will provide [Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/). ## Background - - They key challenge is to create a well-performant API compliant with the specification. Performance is seen as one of the most imporatant charactristics of logging libraries in Go. @@ -145,8 +141,6 @@ and interoperable with existing logging packages.[^1] ## Rationale - - ### Logger.Emit definition One of the ideas was to have: From 9d9f10bc09b3304405454a559970e40eb1ba859a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 15:41:17 +0100 Subject: [PATCH 07/83] Cleanup --- log/DESIGN.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index eca4dbdb658..8e95167b34e 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -1,8 +1,6 @@ # Logs Bridge API -Author: Robert Pająk - -Tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/issues/4696). +OpenTelemetry Logs tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/issues/4696). ## Abstract From 55bad1e9586e8b914cc38d720506c21e980518e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 15:46:03 +0100 Subject: [PATCH 08/83] Fix typo --- log/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 8e95167b34e..9e1f36237df 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -160,7 +160,7 @@ func log(ctx context.Context, l Logger, options ...RecordOption) { } ``` -The main reasons against this defintion are that following: +The main reasons against this definition are that following: 1. The existing design is similar to the [Meter API](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Meter) for creating instruments. From 249f4ed4e90ba15c1a380ff21ebc118c20ce0846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 16:18:40 +0100 Subject: [PATCH 09/83] Apply suggestions from code review Co-authored-by: David Ashpole --- log/DESIGN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 9e1f36237df..4a0955789d6 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -139,7 +139,7 @@ and interoperable with existing logging packages.[^1] ## Rationale -### Logger.Emit definition +### Rejected Alternative: Record as explicit argument to Logger.Emit One of the ideas was to have: @@ -167,7 +167,7 @@ for creating instruments. 2. It is unsure if anyone would like to reuse a record. 3. Just passing options should be more-user friendly API. -### Record as struct +### Rejected Alternative: Record as interface `Record` is defined as a `struct` because of the following reasons. From 33a8acaa6b132d4c45a0f796cbf67dbff47f3c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Nov 2023 16:28:14 +0100 Subject: [PATCH 10/83] Clairfy RecordOption --- log/DESIGN.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/log/DESIGN.md b/log/DESIGN.md index 4a0955789d6..7bc0420b826 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -54,6 +54,9 @@ type Logger interface{ The `Logger` has `Emit(context.Context, options ...RecordOption` method. +The options are used to set log record parameters e.g. `WithBody`, `WithTimestamp`. +There would NOT be a `WithLogRecord` option. + ### Record The [`LogRecord` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) @@ -166,6 +169,10 @@ The main reasons against this definition are that following: for creating instruments. 2. It is unsure if anyone would like to reuse a record. 3. Just passing options should be more-user friendly API. +4. The user does not need to check if the record is valid. + The SDK handles the error returned from `NewRecord`. + If the API would accept a `Record` then the SDK would need to e.g. validate the Severity value. + Now the validation can be part of `NewRecord`. ### Rejected Alternative: Record as interface From 2e3587ce027257d224873b99ca89cb77169a68d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 21 Nov 2023 08:44:35 +0100 Subject: [PATCH 11/83] document fields that optimize attrs --- log/DESIGN.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 7bc0420b826..6fa932aa524 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -70,15 +70,18 @@ type Record struct { 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 Attrs. + // code). It holds the start of the list of attributes. front [nAttrsInline]attribute.KeyValue - // The number of Attrs in front. + // The number of attributes in front. nFront int - // The list of Attrs except for those in front. + // 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. From f19f0c8d67e69072a77b628d72d05a7ef301230a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 21 Nov 2023 08:46:51 +0100 Subject: [PATCH 12/83] Add missing parenthesis --- log/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 6fa932aa524..5c05a8429ac 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -52,7 +52,7 @@ type Logger interface{ } ``` -The `Logger` has `Emit(context.Context, options ...RecordOption` method. +The `Logger` has `Emit(context.Context, options ...RecordOption)` method. The options are used to set log record parameters e.g. `WithBody`, `WithTimestamp`. There would NOT be a `WithLogRecord` option. From 5456b9de460616f37fe8d06cacf5b555937eef55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 21 Nov 2023 08:55:42 +0100 Subject: [PATCH 13/83] Record and NewRecord is needed for SDK --- log/DESIGN.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 5c05a8429ac..f858c74acc2 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -127,11 +127,15 @@ and [`slog.Record.AddAttrs`](https://pkg.go.dev/log/slog#Record.AddAttrs), in order to achieve high-performance when accessing and setting attributes efficiently. The `NewRecord(...RecordOption) (Record, error)` is a factory function -used to create records using provided options. +used to create a record using the passed options. `Record` has a `Clone` method to allow copying records so that the SDK can offer concurrency safety. +The `Record` type and `NewRecord` function are needed for the SDK +to process the options passed by the user via `Logger.Emit`. +API users would not use it in their production code. + ## Compatibility The backwards compatibility is achieved using the `embedded` design pattern @@ -157,7 +161,7 @@ type Logger interface{ This gives the advantage that the SDK would not need to call `NewRecord(options...)`. -The user can still easily create a helper that could be easier to use: +The API user can still easily create a helper that could be easier to use: ```go func log(ctx context.Context, l Logger, options ...RecordOption) { @@ -172,7 +176,7 @@ The main reasons against this definition are that following: for creating instruments. 2. It is unsure if anyone would like to reuse a record. 3. Just passing options should be more-user friendly API. -4. The user does not need to check if the record is valid. +4. The API user does not need to check if the record is valid. The SDK handles the error returned from `NewRecord`. If the API would accept a `Record` then the SDK would need to e.g. validate the Severity value. Now the validation can be part of `NewRecord`. From 4365860c10d4db73d4f6fed83585bb9389ca44e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 21 Nov 2023 09:50:42 +0100 Subject: [PATCH 14/83] Add usage examples --- log/DESIGN.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index f858c74acc2..d2d71086532 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -36,7 +36,7 @@ is defined as an interface. ```go type LoggerProvider interface{ embedded.LoggerProvider - Logger(name string, options ...LoggerOption) Logger + Logger(name string, options ...LoggerOption) Logger } ``` @@ -48,7 +48,7 @@ is defined as an interface. ```go type Logger interface{ embedded.Logger - Emit(ctx context.Context, options ...RecordOption) + Emit(ctx context.Context, options ...RecordOption) } ``` @@ -129,6 +129,10 @@ in order to achieve high-performance when accessing and setting attributes effic The `NewRecord(...RecordOption) (Record, error)` is a factory function used to create a record using the passed options. +`Record` has a `AttributesLen` method that returns +the number of attributes to allow slice preallocation +when converting records to a different representation. + `Record` has a `Clone` method to allow copying records so that the SDK can offer concurrency safety. @@ -136,6 +140,70 @@ The `Record` type and `NewRecord` function are needed for the SDK to process the options passed by the user via `Logger.Emit`. API users would not use it in their production code. +### Usage Example: Log Bridge implementation + +Excerpt of a [slog.Handler](https://pkg.go.dev/log/slog#Handler) +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 { + lvl := convertLevel(r.Level) + + attrs := make([]attribute.KeyValue, 0, len(r.NumAttrs())) + r.Attrs(func(a slog.Attr) bool { + attrs = append(attrs, convertAttr(h.prefix, a)) + return true + }) + + h.logger.Emit(ctx, + log.WithTimestamp(r.Time), + log.WithSeverity(lvl), + log.WithBody(r.Message), + log.WithAttributes(attrs...), + ) + return nil +} +``` + +### Usage Example: Direct API usage + +The users may also chose to use the API directly. + +```go +logger := otel.Logger("my-service") +logger.Emit(ctx, log.WithSeverity(log.SeverityInfo), log.WithBody("Application started.")) +``` + +### Usage Example: SDK implementation + +Excerpt of how SDK can implement the `Logger` interface. + +```go +type Logger struct { + scope instrumentation.Scope + processor Processor +} + +func (l *Logger) Emit(ctx context.Context, opts ...log.RecordOption) { + r, err := log.NewRecord(opts...) + if err != nil { + otel.Handle(err) + return + } + + // Create log record model. + record := toModel(r) + l.processor.Process(ctx, record) +} +``` + ## Compatibility The backwards compatibility is achieved using the `embedded` design pattern From 892ee8f9fc404ecf7a58d0b4897386bb145cccd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 21 Nov 2023 10:16:16 +0100 Subject: [PATCH 15/83] Document Rejected Alternative: Reuse slog --- log/DESIGN.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/log/DESIGN.md b/log/DESIGN.md index d2d71086532..8a65a65a10f 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -264,6 +264,20 @@ usage of intefaces tend to increase heap allocations.[^2] The `Record` design is inspired by [`slog.Record`](https://pkg.go.dev/log/slog#Record). +### Rejected Alternative: Reuse slog + +The API must not be coupled to [`slog`](https://pkg.go.dev/log/slog), +nor any other logging library. + +The API needs to evolve orthogonally to `slog`. + +`slog` is not compliant with the [Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/). +and we cannot expect the Go team to make `slog` compliant with it. + +The interoperabilty can be achieved using [a log bridge](https://opentelemetry.io/docs/specs/otel/glossary/#log-appender--bridge). + +You can read more about OpenTelemetry Logs design on [opentelemetry.io](https://opentelemetry.io/docs/concepts/signals/logs/). + ## Open issues (if applicable) [^1]: Jonathan Amsterdam, [The Go Blog: Structured Logging with slog](https://go.dev/blog/slog) [^2]: Jonathan Amsterdam, [GopherCon Europe 2023: A Fast Structured Logging Package](https://www.youtube.com/watch?v=tC4Jt3i62ns) +[^3]: [Emit definition discussion with benchmarks](https://github.com/open-telemetry/opentelemetry-go/pull/4725#discussion_r1400869566) From c0a795bccd3d173758df23f82fd25b4bfd32fdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 22 Nov 2023 14:45:53 +0100 Subject: [PATCH 17/83] Document Logger.Emit extensibility --- log/DESIGN.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/log/DESIGN.md b/log/DESIGN.md index 1a42fd98331..afd7b8c9594 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -198,6 +198,9 @@ func (l *Logger) Emit(ctx context.Context, r log.Record) { The backwards compatibility is achieved using the `embedded` design pattern that is already used in Trace API and Metrics API. +Additionally, the `Logger.Emit` functionality can be extended by +adding new exported fields and methods to the `Record` struct. + ## Benchmarking The benchmarks takes inspiration from [`slog`](https://pkg.go.dev/log/slog), From 2704436ed2678060dadfae423b94703167dfcb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 08:44:22 +0100 Subject: [PATCH 18/83] Apply suggestions from code review Co-authored-by: Remy Chantenay --- log/DESIGN.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index afd7b8c9594..df8872ba269 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -9,8 +9,8 @@ We propose adding a `go.opentelemetry.io/otel/log` Go module which will provide ## Background -They key challenge is to create a well-performant API compliant with the specification. -Performance is seen as one of the most imporatant charactristics of logging libraries in Go. +The key challenge is to create a well-performant API compliant with the specification. +Performance is seen as one of the most important characteristics of logging libraries in Go. ## Design @@ -234,7 +234,7 @@ The log record resembles the instrument config structs like [metric.Float64Count Using `struct` instead of `interface` should have better the performance as e.g. indirect calls are less optimized, -usage of intefaces tend to increase heap allocations.[^2] +usage of interfaces tend to increase heap allocations.[^2] The `Record` design is inspired by [`slog.Record`](https://pkg.go.dev/log/slog#Record). From a718f69cf3e15bd9b63b5cfcf5ddfe6aab83502e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 14:43:52 +0100 Subject: [PATCH 19/83] Write prototype implementation and benchmarks --- log/DESIGN.md | 77 +--------------- log/bench_test.go | 30 ------- log/benchmark/bench_test.go | 170 ++++++++++++++++++++++++++++++++++++ log/benchmark/impl.go | 67 ++++++++++++++ log/benchmark/impl_test.go | 34 ++++++++ log/embedded/embedded.go | 35 ++++++++ log/go.mod | 21 +++++ log/go.sum | 16 ++++ log/logger.go | 18 ++++ log/noop/noop.go | 47 ++++++++++ log/provider.go | 92 +++++++++++++++++++ log/record.go | 155 ++++++++++++++++++++++++++++++++ 12 files changed, 659 insertions(+), 103 deletions(-) delete mode 100644 log/bench_test.go create mode 100644 log/benchmark/bench_test.go create mode 100644 log/benchmark/impl.go create mode 100644 log/benchmark/impl_test.go create mode 100644 log/embedded/embedded.go create mode 100644 log/go.sum create mode 100644 log/logger.go create mode 100644 log/noop/noop.go create mode 100644 log/provider.go create mode 100644 log/record.go 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 +} From 700c91f4d2a0d7d655acbf84718fd2f57df25af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 14:46:29 +0100 Subject: [PATCH 20/83] Add missing vanity import --- log/benchmark/impl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/benchmark/impl.go b/log/benchmark/impl.go index 28430c30470..fbc77e62526 100644 --- a/log/benchmark/impl.go +++ b/log/benchmark/impl.go @@ -5,7 +5,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package benchmark +package benchmark // import "go.opentelemetry.io/otel/log/benchmark" import ( "context" From 2e8403e15e9fcea2989f76b05269283dd1af9bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 14:49:59 +0100 Subject: [PATCH 21/83] gofumpt --- log/benchmark/impl_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/log/benchmark/impl_test.go b/log/benchmark/impl_test.go index f4c782b044f..b2cd9437313 100644 --- a/log/benchmark/impl_test.go +++ b/log/benchmark/impl_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) From 441cde81635f577497d30a8a1067fe307cc5156b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 14:53:49 +0100 Subject: [PATCH 22/83] Add TODO comments --- log/provider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/log/provider.go b/log/provider.go index c1f78d10440..baa8a0d508b 100644 --- a/log/provider.go +++ b/log/provider.go @@ -51,6 +51,7 @@ type LoggerOption interface { // NewLoggerConfig creates a new LoggerConfig and applies // all the given options. +// TODO: Add unit tests. func NewLoggerConfig(opts ...LoggerOption) LoggerConfig { var config LoggerConfig for _, o := range opts { From de09ce6dc78e6d08cf2e5d0df09d95c8c1741a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 15:00:08 +0100 Subject: [PATCH 23/83] Add copywrite header and fix for Go 1.20 --- log/record.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/log/record.go b/log/record.go index 7ab48cccaa9..c5b8cd9d7fe 100644 --- a/log/record.go +++ b/log/record.go @@ -1,11 +1,14 @@ // 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 ( "errors" - "slices" "time" "go.opentelemetry.io/otel" @@ -117,12 +120,12 @@ func (r *Record) AddAttributes(attrs ...attribute.KeyValue) { 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) + r.back = sliceClip(r.back) otel.Handle(errUnsafeAddAttrs) } } ne := countInvalidAttrs(attrs[i:]) - r.back = slices.Grow(r.back, len(attrs[i:])-ne) + r.back = sliceGrow(r.back, len(attrs[i:])-ne) for _, a := range attrs[i:] { if a.Valid() { r.back = append(r.back, a) @@ -134,7 +137,7 @@ func (r *Record) AddAttributes(attrs ...attribute.KeyValue) { // 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 + r.back = sliceClip(r.back) // prevent append from mutating shared array return r } @@ -153,3 +156,26 @@ func countInvalidAttrs(as []attribute.KeyValue) int { } return n } + +// 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 < 0 { + panic("cannot be negative") + } + if n -= cap(s) - len(s); n > 0 { + s = append(s[:cap(s)], make([]E, n)...)[:len(s)] + } + return s +} + +// sliceClip removes unused capacity from the slice, returning s[:len(s):len(s)]. +// +// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. +func sliceClip[S ~[]E, E any](s S) S { + return s[:len(s):len(s)] +} From dad0503463ec980b3e5a6cb85bf0b8be70c8850a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 15:13:43 +0100 Subject: [PATCH 24/83] refactor --- log/benchmark/impl_test.go | 35 ------------------- .../{impl.go => writer_logger_test.go} | 27 +++++++++++--- 2 files changed, 22 insertions(+), 40 deletions(-) delete mode 100644 log/benchmark/impl_test.go rename log/benchmark/{impl.go => writer_logger_test.go} (70%) diff --git a/log/benchmark/impl_test.go b/log/benchmark/impl_test.go deleted file mode 100644 index b2cd9437313..00000000000 --- a/log/benchmark/impl_test.go +++ /dev/null @@ -1,35 +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 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/benchmark/impl.go b/log/benchmark/writer_logger_test.go similarity index 70% rename from log/benchmark/impl.go rename to log/benchmark/writer_logger_test.go index fbc77e62526..8fc419bc58e 100644 --- a/log/benchmark/impl.go +++ b/log/benchmark/writer_logger_test.go @@ -1,23 +1,40 @@ // 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 "go.opentelemetry.io/otel/log/benchmark" +package benchmark import ( "context" "fmt" "io" "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" ) +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()) +} + // 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. From c43bd517f71fa6d7870c9723102bf201184adb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 15:15:21 +0100 Subject: [PATCH 25/83] Update design --- log/DESIGN.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index c5277fad0d3..e7dc1b6a0f2 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -31,14 +31,7 @@ The Go module consits of the following packages: ### LoggerProvider The [`LoggerProvider` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#loggerprovider) -is defined as an interface. - -```go -type LoggerProvider interface{ - embedded.LoggerProvider - Logger(name string, options ...LoggerOption) Logger -} -``` +is defined as an interface [provider.go](provider.go). ### Logger From 19438ca54a153e6bc23ce62c6cd9f449fe5263c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 15:17:02 +0100 Subject: [PATCH 26/83] Update design --- log/DESIGN.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index e7dc1b6a0f2..293ae1a1926 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -92,7 +92,11 @@ func (h *handler) Handle(ctx context.Context, r slog.Record) error { The users may also chose to use the API directly. ```go -logger := otel.Logger("my-service") +package app + +var logger = otel.Logger("my-service") + +// In some function: logger.Emit(ctx, Record{Severity: log.SeverityInfo, Body: "Application started."}) ``` From d4896f64a61f6dba323807d42a22daa1158c1d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 24 Nov 2023 15:31:28 +0100 Subject: [PATCH 27/83] Rejected Alternative: Passing struct as parameter to LoggerProvider.Logger --- log/DESIGN.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/log/DESIGN.md b/log/DESIGN.md index 293ae1a1926..5faeab39022 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -204,6 +204,26 @@ It should reduce the possibility of a heap allocation. The user would not be able to pass `nil`. Therefore, it reduces the possiblity to have a nil pointer dereference. +### Rejected Alternative: Passing struct as parameter to LoggerProvider.Logger + +Similarly to `Logger.Emit`, we could have something like: + +```go +type Logger interface{ + embedded.Logger + Logger(name context.Context, config LoggerConfig) +} +``` + +The drawback of this idea would be that this would be +a different design from Trace and Metrics API. + +The performance of acquiring a logger is not as critical +as the performance of emitting a log record. While a single +HTTP/RPC handler could write hundreds of logs, it should not +create a new logger for each log entry. +The application should reuse loggers whenever possible. + ## Open issues (if applicable) [^1]: Jonathan Amsterdam, [The Go Blog: Structured Logging with slog](https://go.dev/blog/slog) [^2]: Jonathan Amsterdam, [GopherCon Europe 2023: A Fast Structured Logging Package](https://www.youtube.com/watch?v=tC4Jt3i62ns) [^3]: [Emit definition discussion with benchmarks](https://github.com/open-telemetry/opentelemetry-go/pull/4725#discussion_r1400869566) +[^4]: [Logger.WithAttributes analysis](https://github.com/pellared/opentelemetry-go/pull/3) +[^5]: [Record attributes as field and use sync.Pool for reducing allocations analysis](https://github.com/pellared/opentelemetry-go/pull/4) diff --git a/log/benchmark/bench_test.go b/log/benchmark/bench_test.go index c3713c036cc..c6c297f0d22 100644 --- a/log/benchmark/bench_test.go +++ b/log/benchmark/bench_test.go @@ -14,6 +14,7 @@ package benchmark import ( "context" "io" + "sync" "testing" "time" @@ -43,6 +44,13 @@ var ( // slow logger implementation is skewing the results. The writerLogger // allocates memory only when using strconv. func BenchmarkEmit(b *testing.B) { + attrPool := sync.Pool{ + New: func() interface{} { + attr := make([]attribute.KeyValue, 0, 5) + return &attr + }, + } + for _, tc := range []struct { name string logger log.Logger @@ -58,25 +66,34 @@ func BenchmarkEmit(b *testing.B) { { "no attrs", func() { - r := log.Record{} - r.SetTimestamp(testTimestamp) - r.SetSeverity(testSeverity) - r.SetBody(testBody) + r := log.Record{ + Timestamp: testTimestamp, + Severity: testSeverity, + Body: testBody, + } tc.logger.Emit(ctx, r) }, }, { "3 attrs", func() { - r := log.Record{} - r.SetTimestamp(testTimestamp) - r.SetSeverity(testSeverity) - r.SetBody(testBody) - r.AddAttributes( + ptr := attrPool.Get().(*[]attribute.KeyValue) + attrs := *ptr + defer func() { + *ptr = attrs[:0] + attrPool.Put(ptr) + }() + attrs = append(attrs, attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), ) + r := log.Record{ + Timestamp: testTimestamp, + Severity: testSeverity, + Body: testBody, + Attributes: attrs, + } tc.logger.Emit(ctx, r) }, }, @@ -87,28 +104,38 @@ func BenchmarkEmit(b *testing.B) { // 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( + ptr := attrPool.Get().(*[]attribute.KeyValue) + attrs := *ptr + defer func() { + *ptr = attrs[:0] + attrPool.Put(ptr) + }() + attrs = append(attrs, attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), attribute.Bool("bool", testBool), attribute.String("string", testString), ) + r := log.Record{ + Timestamp: testTimestamp, + Severity: testSeverity, + Body: testBody, + Attributes: attrs, + } tc.logger.Emit(ctx, r) }, }, { "10 attrs", func() { - r := log.Record{} - r.SetTimestamp(testTimestamp) - r.SetSeverity(testSeverity) - r.SetBody(testBody) - r.AddAttributes( + ptr := attrPool.Get().(*[]attribute.KeyValue) + attrs := *ptr + defer func() { + *ptr = attrs[:0] + attrPool.Put(ptr) + }() + attrs = append(attrs, attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), @@ -120,17 +147,25 @@ func BenchmarkEmit(b *testing.B) { attribute.Bool("bool", testBool), attribute.String("string", testString), ) + r := log.Record{ + Timestamp: testTimestamp, + Severity: testSeverity, + Body: testBody, + Attributes: attrs, + } tc.logger.Emit(ctx, r) }, }, { "40 attrs", func() { - r := log.Record{} - r.SetTimestamp(testTimestamp) - r.SetSeverity(testSeverity) - r.SetBody(testBody) - r.AddAttributes( + ptr := attrPool.Get().(*[]attribute.KeyValue) + attrs := *ptr + defer func() { + *ptr = attrs[:0] + attrPool.Put(ptr) + }() + attrs = append(attrs, attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), @@ -172,6 +207,12 @@ func BenchmarkEmit(b *testing.B) { attribute.Bool("bool", testBool), attribute.String("string", testString), ) + r := log.Record{ + Timestamp: testTimestamp, + Severity: testSeverity, + Body: testBody, + Attributes: attrs, + } tc.logger.Emit(ctx, r) }, }, diff --git a/log/benchmark/go.mod b/log/benchmark/go.mod index 293569e0cf6..e389e5dc55c 100644 --- a/log/benchmark/go.mod +++ b/log/benchmark/go.mod @@ -12,17 +12,14 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // 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/log => ../ replace go.opentelemetry.io/otel => ../.. + +replace go.opentelemetry.io/otel/trace => ../../trace + +replace go.opentelemetry.io/otel/metric => ../../metric diff --git a/log/benchmark/go.sum b/log/benchmark/go.sum index 5b5bdfca2d9..df42623c7b1 100644 --- a/log/benchmark/go.sum +++ b/log/benchmark/go.sum @@ -1,10 +1,7 @@ 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= diff --git a/log/benchmark/logr_test.go b/log/benchmark/logr_test.go index 2ee9c494f58..37f31a9125f 100644 --- a/log/benchmark/logr_test.go +++ b/log/benchmark/logr_test.go @@ -5,6 +5,7 @@ package benchmark import ( "fmt" + "sync" "testing" "github.com/go-logr/logr" @@ -21,20 +22,28 @@ func TestLogrSink(t *testing.T) { l.Info(testBody, "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 attribute.KeyValue) bool { - assert.Equal(t, "string", string(kv.Key)) - assert.Equal(t, testString, kv.Value.AsString()) - return true - }) + want := log.Record{ + Body: testBody, + Severity: log.SeverityInfo, + Attributes: []attribute.KeyValue{ + attribute.String("string", testString), + }, + } + + assert.Equal(t, want, spy.Record) } type logrSink struct { Logger log.Logger } +var logrAttrPool = sync.Pool{ + New: func() interface{} { + attr := make([]attribute.KeyValue, 0, 5) + return &attr + }, +} + // Init is implementated as a dummy. func (s *logrSink) Init(info logr.RuntimeInfo) { } @@ -49,23 +58,30 @@ func (s *logrSink) Enabled(level int) bool { func (s *logrSink) Info(level int, msg string, keysAndValues ...any) { record := log.Record{} - record.SetBody(msg) + record.Body = msg lvl := log.Severity(9 - level) - record.SetSeverity(lvl) + record.Severity = lvl if len(keysAndValues)%2 == 1 { panic("key without a value") } kvCount := len(keysAndValues) / 2 + ptr := logrAttrPool.Get().(*[]attribute.KeyValue) + attrs := *ptr + defer func() { + *ptr = attrs[:0] + logrAttrPool.Put(ptr) + }() for i := 0; i < kvCount; i++ { k, ok := keysAndValues[i*2].(string) if !ok { panic("key is not a string") } kv := convertKV(k, keysAndValues[i*2+1]) - record.AddAttributes(kv) + attrs = append(attrs, kv) } + record.Attributes = attrs s.Logger.Emit(ctx, record) } diff --git a/log/benchmark/slog_test.go b/log/benchmark/slog_test.go index c076ae2ce81..5e5d796f223 100644 --- a/log/benchmark/slog_test.go +++ b/log/benchmark/slog_test.go @@ -6,7 +6,9 @@ package benchmark import ( "context" "fmt" + "sync" "testing" + "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" @@ -21,36 +23,53 @@ func TestSlogHandler(t *testing.T) { l.Info(testBody, "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 attribute.KeyValue) bool { - assert.Equal(t, "string", string(kv.Key)) - assert.Equal(t, testString, kv.Value.AsString()) - return true - }) + want := log.Record{ + Body: testBody, + Severity: log.SeverityInfo, + Attributes: []attribute.KeyValue{ + attribute.String("string", testString), + }, + } + + assert.NotZero(t, spy.Record.Timestamp, "should set a timestamp") + spy.Record.Timestamp = time.Time{} + assert.Equal(t, want, spy.Record) } type slogHandler struct { Logger log.Logger } +var slogAttrPool = sync.Pool{ + New: func() interface{} { + attr := make([]attribute.KeyValue, 0, 5) + return &attr + }, +} + // Handle handles the Record. // It should avoid memory allocations whenever possible. func (h *slogHandler) Handle(_ context.Context, r slog.Record) error { record := log.Record{} - record.SetTimestamp(r.Time) + record.Timestamp = r.Time - record.SetBody(r.Message) + record.Body = r.Message lvl := convertLevel(r.Level) - record.SetSeverity(lvl) - + record.Severity = lvl + + ptr := slogAttrPool.Get().(*[]attribute.KeyValue) + attrs := *ptr + defer func() { + *ptr = attrs[:0] + slogAttrPool.Put(ptr) + }() r.Attrs(func(a slog.Attr) bool { - record.AddAttributes(convertAttr(a)) + attrs = append(attrs, convertAttr(a)) return true }) + record.Attributes = attrs h.Logger.Emit(context.Background(), record) return nil diff --git a/log/benchmark/writer_logger_test.go b/log/benchmark/writer_logger_test.go index b1043cc25fb..a0eb4d2bbe6 100644 --- a/log/benchmark/writer_logger_test.go +++ b/log/benchmark/writer_logger_test.go @@ -22,16 +22,17 @@ 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( - attribute.String("string", testString), - attribute.Float64("float", testFloat), - attribute.Int("int", testInt), - attribute.Bool("bool", testBool), - ) + r := log.Record{ + Timestamp: testTimestamp, + Severity: testSeverity, + Body: testBody, + Attributes: []attribute.KeyValue{ + 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" @@ -47,23 +48,22 @@ type writerLogger struct { } func (l *writerLogger) Emit(_ context.Context, r log.Record) { - if !r.Timestamp().IsZero() { + if !r.Timestamp.IsZero() { l.write("timestamp=") - l.write(strconv.FormatInt(r.Timestamp().Unix(), 10)) + l.write(strconv.FormatInt(r.Timestamp.Unix(), 10)) l.write(" ") } l.write("severity=") - l.write(strconv.FormatInt(int64(r.Severity()), 10)) + l.write(strconv.FormatInt(int64(r.Severity), 10)) l.write(" ") l.write("body=") - l.write(r.Body()) - r.WalkAttributes(func(kv attribute.KeyValue) bool { + l.write(r.Body) + for _, kv := range r.Attributes { l.write(" ") l.write(string(kv.Key)) l.write("=") l.appendValue(kv.Value) - return true - }) + } l.write("\n") } diff --git a/log/go.mod b/log/go.mod index e073bbf54c4..abb4f8c45eb 100644 --- a/log/go.mod +++ b/log/go.mod @@ -4,15 +4,8 @@ go 1.20 require go.opentelemetry.io/otel v1.21.0 -require ( - github.com/go-logr/logr v1.3.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect -) +replace go.opentelemetry.io/otel => ../ 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 index a90f90be690..2d8ac4840ed 100644 --- a/log/go.sum +++ b/log/go.sum @@ -1,9 +1,4 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/log/record.go b/log/record.go index 70cd95cd853..d4e3d60c064 100644 --- a/log/record.go +++ b/log/record.go @@ -8,44 +8,21 @@ package log // import "go.opentelemetry.io/otel/log" import ( - "errors" "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 { - 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 [attributesInlineCount]attribute.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 []attribute.KeyValue + Timestamp time.Time + ObservedTimestamp time.Time + Severity Severity + SeverityText string + Body string + Attributes []attribute.KeyValue } -const attributesInlineCount = 5 - // Severity TODO: comment. type Severity int @@ -77,146 +54,3 @@ const ( SeverityFatal3 SeverityFatal4 ) - -// Timestamp TODO: comment. -func (r Record) Timestamp() time.Time { - return r.timestamp -} - -// SetTimestamp TODO: comment. -func (r *Record) SetTimestamp(t time.Time) { - r.timestamp = t -} - -// ObservedTimestamp TODO: comment. -func (r Record) ObservedTimestamp() time.Time { - return r.observedTimestamp -} - -// SetObservedTimestamp TODO: comment. -func (r *Record) SetObservedTimestamp(t time.Time) { - r.observedTimestamp = t -} - -// Severity TODO: comment. -func (r Record) Severity() Severity { - return r.severity -} - -// SetSeverity TODO: comment. -func (r *Record) SetSeverity(s Severity) { - r.severity = s -} - -// SeverityText TODO: comment. -func (r Record) SeverityText() string { - return r.severityText -} - -// SetSeverityText TODO: comment. -func (r *Record) SetSeverityText(s string) { - r.severityText = s -} - -// Body TODO: comment. -func (r Record) Body() string { - return r.body -} - -// SetBody TODO: comment. -func (r *Record) SetBody(s string) { - r.body = s -} - -// WalkAttributes calls f on each [attribute.KeyValue] in the [Record]. -// Iteration stops if f returns false. -func (r Record) WalkAttributes(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 = sliceClip(r.back) - otel.Handle(errUnsafeAddAttrs) - } - } - ne := countInvalidAttrs(attrs[i:]) - r.back = sliceGrow(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 = sliceClip(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 -} - -// 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 < 0 { - panic("cannot be negative") - } - if n -= cap(s) - len(s); n > 0 { - s = append(s[:cap(s)], make([]E, n)...)[:len(s)] - } - return s -} - -// sliceClip removes unused capacity from the slice, returning s[:len(s):len(s)]. -// -// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. -func sliceClip[S ~[]E, E any](s S) S { - return s[:len(s):len(s)] -} From 033e2e90464b8c9adb0ed995ace7fdf998a89338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 09:49:11 +0100 Subject: [PATCH 47/83] go mod tidy --- log/benchmark/go.mod | 2 +- log/benchmark/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/log/benchmark/go.mod b/log/benchmark/go.mod index e389e5dc55c..ecdebe8d963 100644 --- a/log/benchmark/go.mod +++ b/log/benchmark/go.mod @@ -3,7 +3,7 @@ module go.opentelemetry.io/otel/log/benchmark go 1.20 require ( - github.com/go-logr/logr v1.3.0 + github.com/go-logr/logr v1.4.1 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 diff --git a/log/benchmark/go.sum b/log/benchmark/go.sum index df42623c7b1..949da0b69c3 100644 --- a/log/benchmark/go.sum +++ b/log/benchmark/go.sum @@ -1,7 +1,7 @@ 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.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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= From 8c8f7ab12f9ddb8c5f08f49a22bce83b54afc5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 09:49:50 +0100 Subject: [PATCH 48/83] Trace contextg correlation design --- log/DESIGN.md | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 584672f2328..54b1d23fca2 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -38,6 +38,13 @@ is defined as an interface [provider.go](provider.go). The [`Logger` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) is defined as an interface in [logger.go](logger.go). +Canceling the context pass to `Emit` should not affect record processing. +Among other things, log messages may be necessary to debug a +cancellation-related problem. +The context is used to pass request-scoped values. +The API implementation should handle the trace context passed +in `ctx` to the `Emit` method. + ### Record The [`LogRecord` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) @@ -47,6 +54,30 @@ is defined as a struct in [record.go](record.go). ### Log Bridge implementation +The log bridges can use [`sync.Pool`](https://pkg.go.dev/sync#Pool) +for reducing the number of allocations when mapping attributes. + +The bridge implementation should do its best to pass +the `ctx` containing the trace context from the call-site +so it can later passed via `Emit`. +Re-constructing a `context.Context` with [`trace.ContextWithSpanContext`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#ContextWithSpanContext) +and [`trace.NewSpanContext`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#NewSpanContext) +would usually involve more memory allocations. + +The logging libraries which have recording methods that accepts `context.Context`, +such us [`slog`](https://pkg.go.dev/log/slog), +[`logrus`](https://pkg.go.dev/github.com/sirupsen/logrus) +[`zerolog`](https://pkg.go.dev/github.com/rs/zerolog), +makes passing the trace context trivial. + +However, some libraries do not accept a `context.Context` in their recording methods. +Structured logging libraries, +such as [`logr`](https://pkg.go.dev/github.com/go-logr/logr) +and [`zap`](https://pkg.go.dev/go.uber.org/zap), +offer passing `any` type as a log attribute/field. +Therefore, their bridge implementations can define a "special" log attributes/field +that will be used to capture the trace context. + A naive implementation of the [slog.Handler](https://pkg.go.dev/log/slog#Handler) interface is in [benchmark/slog_test.go](benchmark/slog_test.go). @@ -55,9 +86,6 @@ A naive implementation of the [logr.LogSink](https://pkg.go.dev/github.com/go-logr/logr#LogSink) interface is in [benchmark/logr_test.go](benchmark/slog_test.go). -The log bridges can use [`sync.Pool`](https://pkg.go.dev/sync#Pool) -for reducing the number of allocations when mapping attributes. - ### Direct API usage The users may also chose to use the API directly. @@ -92,17 +120,12 @@ func (l *Logger) Emit(ctx context.Context, r log.Record) { } ``` -A test implementation of the the `Logger` interface -is in [benchmark/writer_logger_test.go](benchmark/writer_logger_test.go). - If the record is processed asynchronously, then the processor has to copy record attributes, in order to avoid use after free bugs and race condition. -Canceling the context should not affect record processing. -Among other things, log messages may be necessary to debug a -cancellation-related problem. -The context is used to pass request-scoped values such as Trace ID and Span ID. +A test implementation of the the `Logger` interface +is in [benchmark/writer_logger_test.go](benchmark/writer_logger_test.go). ## Compatibility From a79b08ff64e293de8d746cb5c5deede77c05cec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 10:07:05 +0100 Subject: [PATCH 49/83] writerLogger and slogHandler handle trace context --- log/benchmark/bench_test.go | 3 ++- log/benchmark/slog_test.go | 7 ++++--- log/benchmark/spy_test.go | 6 ++++-- log/benchmark/writer_logger_test.go | 11 +++++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/log/benchmark/bench_test.go b/log/benchmark/bench_test.go index c6c297f0d22..6a372e54dff 100644 --- a/log/benchmark/bench_test.go +++ b/log/benchmark/bench_test.go @@ -21,13 +21,14 @@ import ( "go.opentelemetry.io/otel/attribute" "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 = context.Background() + 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) testBody = "log message" testSeverity = log.SeverityInfo diff --git a/log/benchmark/slog_test.go b/log/benchmark/slog_test.go index 5e5d796f223..85e2470193c 100644 --- a/log/benchmark/slog_test.go +++ b/log/benchmark/slog_test.go @@ -21,7 +21,7 @@ func TestSlogHandler(t *testing.T) { spy := &spyLogger{} l := slog.New(&slogHandler{spy}) - l.Info(testBody, "string", testString) + l.InfoContext(ctx, testBody, "string", testString) want := log.Record{ Body: testBody, @@ -34,6 +34,7 @@ func TestSlogHandler(t *testing.T) { assert.NotZero(t, spy.Record.Timestamp, "should set a timestamp") spy.Record.Timestamp = time.Time{} assert.Equal(t, want, spy.Record) + assert.Equal(t, ctx, spy.Context) } type slogHandler struct { @@ -49,7 +50,7 @@ var slogAttrPool = sync.Pool{ // Handle handles the Record. // It should avoid memory allocations whenever possible. -func (h *slogHandler) Handle(_ context.Context, r slog.Record) error { +func (h *slogHandler) Handle(ctx context.Context, r slog.Record) error { record := log.Record{} record.Timestamp = r.Time @@ -71,7 +72,7 @@ func (h *slogHandler) Handle(_ context.Context, r slog.Record) error { }) record.Attributes = attrs - h.Logger.Emit(context.Background(), record) + h.Logger.Emit(ctx, record) return nil } diff --git a/log/benchmark/spy_test.go b/log/benchmark/spy_test.go index 36fa73829fc..4170fa36ba3 100644 --- a/log/benchmark/spy_test.go +++ b/log/benchmark/spy_test.go @@ -12,9 +12,11 @@ import ( type spyLogger struct { embedded.Logger - Record log.Record + Context context.Context + Record log.Record } -func (l *spyLogger) Emit(_ context.Context, r log.Record) { +func (l *spyLogger) Emit(ctx context.Context, r log.Record) { + l.Context = ctx l.Record = r } diff --git a/log/benchmark/writer_logger_test.go b/log/benchmark/writer_logger_test.go index a0eb4d2bbe6..3a0f1f1633a 100644 --- a/log/benchmark/writer_logger_test.go +++ b/log/benchmark/writer_logger_test.go @@ -16,6 +16,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" + "go.opentelemetry.io/otel/trace" ) func TestWriterLogger(t *testing.T) { @@ -35,7 +36,7 @@ func TestWriterLogger(t *testing.T) { } l.Emit(ctx, r) - want := "timestamp=595728000 severity=9 body=log message string=7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190 float=1.2345 int=32768 bool=true\n" + 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()) } @@ -47,7 +48,7 @@ type writerLogger struct { w io.Writer } -func (l *writerLogger) Emit(_ context.Context, r log.Record) { +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)) @@ -64,6 +65,12 @@ func (l *writerLogger) Emit(_ context.Context, r log.Record) { l.write("=") l.appendValue(kv.Value) } + + span := trace.SpanContextFromContext(ctx) + if span.IsValid() { + l.write(" traced=true") + } + l.write("\n") } From 4be490bf1c8a61e5ced20d4c486b39068af2b892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 10:09:27 +0100 Subject: [PATCH 50/83] go mod tidy --- log/benchmark/go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/log/benchmark/go.mod b/log/benchmark/go.mod index ecdebe8d963..815e5a90888 100644 --- a/log/benchmark/go.mod +++ b/log/benchmark/go.mod @@ -7,6 +7,7 @@ require ( github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/otel/trace v1.21.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e ) From 355236849ff42e3488bdc30b118e62d804cc03b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 10:29:26 +0100 Subject: [PATCH 51/83] logrSink handle trace context --- log/benchmark/logr_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/log/benchmark/logr_test.go b/log/benchmark/logr_test.go index 37f31a9125f..a0840418677 100644 --- a/log/benchmark/logr_test.go +++ b/log/benchmark/logr_test.go @@ -4,6 +4,7 @@ package benchmark import ( + "context" "fmt" "sync" "testing" @@ -20,7 +21,7 @@ func TestLogrSink(t *testing.T) { l := logr.New(&logrSink{spy}) - l.Info(testBody, "string", testString) + l.Info(testBody, "string", testString, "ctx", ctx) want := log.Record{ Body: testBody, @@ -31,6 +32,7 @@ func TestLogrSink(t *testing.T) { } assert.Equal(t, want, spy.Record) + assert.Equal(t, ctx, spy.Context) } type logrSink struct { @@ -73,12 +75,19 @@ func (s *logrSink) Info(level int, msg string, keysAndValues ...any) { *ptr = attrs[:0] logrAttrPool.Put(ptr) }() + ctx := context.Background() for i := 0; i < kvCount; i++ { k, ok := keysAndValues[i*2].(string) if !ok { panic("key is not a string") } - kv := convertKV(k, keysAndValues[i*2+1]) + 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) attrs = append(attrs, kv) } record.Attributes = attrs From 3fbebeee18ea11741cbace8bc3d62316fcd9f7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 10:51:11 +0100 Subject: [PATCH 52/83] Refine API implementation section --- log/DESIGN.md | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 54b1d23fca2..362c2b47dd8 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -1,12 +1,13 @@ # Logs Bridge API -OpenTelemetry Logs tracking issue at [#4696](https://github.com/open-telemetry/opentelemetry-go/issues/4696). - ## Abstract -We propose adding a `go.opentelemetry.io/otel/log` Go module which will provide +`go.opentelemetry.io/otel/log` provides [Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/). +The initial version of the design and the prototype +was created in [#4725](https://github.com/open-telemetry/opentelemetry-go/pull/4725). + ## Background The key challenge is to create a well-performant API compliant with the specification. @@ -78,13 +79,10 @@ offer passing `any` type as a log attribute/field. Therefore, their bridge implementations can define a "special" log attributes/field that will be used to capture the trace context. -A naive implementation of -the [slog.Handler](https://pkg.go.dev/log/slog#Handler) interface -is in [benchmark/slog_test.go](benchmark/slog_test.go). - -A naive implementation of -the [logr.LogSink](https://pkg.go.dev/github.com/go-logr/logr#LogSink) interface -is in [benchmark/logr_test.go](benchmark/slog_test.go). +[The prototype](https://github.com/open-telemetry/opentelemetry-go/pull/4725) +has a naive implementation of +[slog.Handler](https://pkg.go.dev/log/slog#Handler) in `benchmark/slog_test.go` +and [logr.LogSink](https://pkg.go.dev/github.com/go-logr/logr#LogSink) in `benchmark/logr_test.go`. ### Direct API usage @@ -101,6 +99,10 @@ logger.Emit(ctx, Record{Severity: log.SeverityInfo, Body: "Application started." ### API implementation +If the implementation processes the asynchronously, +then it has to copy record attributes, +in order to avoid use after free bugs and race condition. + Excerpt of how SDK can implement the `Logger` interface. ```go @@ -116,14 +118,10 @@ func (l *Logger) Emit(ctx context.Context, r log.Record) { otel.Handle(err) return } - l.processor.Process(ctx, record) + l.processor.Process(ctx, record) // Note: A batch processor copies the attributes. } ``` -If the record is processed asynchronously, -then the processor has to copy record attributes, -in order to avoid use after free bugs and race condition. - A test implementation of the the `Logger` interface is in [benchmark/writer_logger_test.go](benchmark/writer_logger_test.go). @@ -292,11 +290,6 @@ We took a different decision, because the key difference is that `slog` is a logging library and Logs Bridge API is only a logging abstraction. We want to provide more flexibility and offer better speed. -## Open issues (if applicable) - - - [^1]: Jonathan Amsterdam, [The Go Blog: Structured Logging with slog](https://go.dev/blog/slog) [^2]: Jonathan Amsterdam, [GopherCon Europe 2023: A Fast Structured Logging Package](https://www.youtube.com/watch?v=tC4Jt3i62ns) [^3]: [Emit definition discussion with benchmarks](https://github.com/open-telemetry/opentelemetry-go/pull/4725#discussion_r1400869566) From 8e25e81006bb4828143201fbecb30b43dac743d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 10:52:23 +0100 Subject: [PATCH 53/83] Fix grammar --- log/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/DESIGN.md b/log/DESIGN.md index 362c2b47dd8..6913b45d074 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -135,7 +135,7 @@ adding new exported fields to the `Record` struct. ## Benchmarking -The benchmarks takes inspiration from [`slog`](https://pkg.go.dev/log/slog), +The benchmarks take inspiration from [`slog`](https://pkg.go.dev/log/slog), because for the Go team it was also critical to create API that would be fast and interoperable with existing logging packages.[^1][^2] From 321a049d93f9d7f2f63834ec911918957b52c17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 27 Dec 2023 11:09:48 +0100 Subject: [PATCH 54/83] Move log/benchmark to log/internal --- .github/dependabot.yml | 2 +- log/DESIGN.md | 6 ++-- log/{benchmark => internal}/bench_test.go | 2 +- log/{benchmark => internal}/go.mod | 2 +- log/{benchmark => internal}/go.sum | 0 .../logr_test.go => internal/logr.go} | 23 +----------- log/internal/logr_test.go | 33 +++++++++++++++++ .../slog_test.go => internal/slog.go} | 25 +------------ log/internal/slog_test.go | 35 +++++++++++++++++++ log/{benchmark => internal}/spy_test.go | 2 +- .../writer_logger.go} | 27 +------------- log/internal/writer_logger_test.go | 35 +++++++++++++++++++ versions.yaml | 2 +- 13 files changed, 114 insertions(+), 80 deletions(-) rename log/{benchmark => internal}/bench_test.go (99%) rename log/{benchmark => internal}/go.mod (93%) rename log/{benchmark => internal}/go.sum (100%) rename log/{benchmark/logr_test.go => internal/logr.go} (83%) create mode 100644 log/internal/logr_test.go rename log/{benchmark/slog_test.go => internal/slog.go} (80%) create mode 100644 log/internal/slog_test.go rename log/{benchmark => internal}/spy_test.go (95%) rename log/{benchmark/writer_logger_test.go => internal/writer_logger.go} (70%) create mode 100644 log/internal/writer_logger_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5bd4f43d664..ee59fe0158b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -227,7 +227,7 @@ updates: interval: weekly day: sunday - package-ecosystem: gomod - directory: /log/benchmark + directory: /log/internal labels: - dependencies - go diff --git a/log/DESIGN.md b/log/DESIGN.md index 6913b45d074..6794abb1bc4 100644 --- a/log/DESIGN.md +++ b/log/DESIGN.md @@ -81,8 +81,8 @@ that will be used to capture the trace context. [The prototype](https://github.com/open-telemetry/opentelemetry-go/pull/4725) has a naive implementation of -[slog.Handler](https://pkg.go.dev/log/slog#Handler) in `benchmark/slog_test.go` -and [logr.LogSink](https://pkg.go.dev/github.com/go-logr/logr#LogSink) in `benchmark/logr_test.go`. +[slog.Handler](https://pkg.go.dev/log/slog#Handler) in `log/internal/slog.go` +and [logr.LogSink](https://pkg.go.dev/github.com/go-logr/logr#LogSink) in `log/internal/logr.go`. ### Direct API usage @@ -123,7 +123,7 @@ func (l *Logger) Emit(ctx context.Context, r log.Record) { ``` A test implementation of the the `Logger` interface -is in [benchmark/writer_logger_test.go](benchmark/writer_logger_test.go). +used for benchmarking is in [internal/writer_logger.go](internal/writer_logger.go). ## Compatibility diff --git a/log/benchmark/bench_test.go b/log/internal/bench_test.go similarity index 99% rename from log/benchmark/bench_test.go rename to log/internal/bench_test.go index 6a372e54dff..b9a11ea1255 100644 --- a/log/benchmark/bench_test.go +++ b/log/internal/bench_test.go @@ -9,7 +9,7 @@ // // They test a complete log record, from the user's call to its return. -package benchmark +package internal import ( "context" diff --git a/log/benchmark/go.mod b/log/internal/go.mod similarity index 93% rename from log/benchmark/go.mod rename to log/internal/go.mod index 815e5a90888..a226081c490 100644 --- a/log/benchmark/go.mod +++ b/log/internal/go.mod @@ -1,4 +1,4 @@ -module go.opentelemetry.io/otel/log/benchmark +module go.opentelemetry.io/otel/log/internal go 1.20 diff --git a/log/benchmark/go.sum b/log/internal/go.sum similarity index 100% rename from log/benchmark/go.sum rename to log/internal/go.sum diff --git a/log/benchmark/logr_test.go b/log/internal/logr.go similarity index 83% rename from log/benchmark/logr_test.go rename to log/internal/logr.go index a0840418677..2a8654a43cc 100644 --- a/log/benchmark/logr_test.go +++ b/log/internal/logr.go @@ -1,40 +1,19 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package benchmark +package internal // import "go.opentelemetry.io/otel/log/internal" import ( "context" "fmt" "sync" - "testing" "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) -func TestLogrSink(t *testing.T) { - spy := &spyLogger{} - - l := logr.New(&logrSink{spy}) - - l.Info(testBody, "string", testString, "ctx", ctx) - - want := log.Record{ - Body: testBody, - Severity: log.SeverityInfo, - Attributes: []attribute.KeyValue{ - attribute.String("string", testString), - }, - } - - assert.Equal(t, want, spy.Record) - assert.Equal(t, ctx, spy.Context) -} - type logrSink struct { Logger log.Logger } diff --git a/log/internal/logr_test.go b/log/internal/logr_test.go new file mode 100644 index 00000000000..3c1a3b5d371 --- /dev/null +++ b/log/internal/logr_test.go @@ -0,0 +1,33 @@ +// 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/attribute" + "go.opentelemetry.io/otel/log" +) + +func TestLogrSink(t *testing.T) { + spy := &spyLogger{} + + l := logr.New(&logrSink{spy}) + + l.Info(testBody, "string", testString, "ctx", ctx) + + want := log.Record{ + Body: testBody, + Severity: log.SeverityInfo, + Attributes: []attribute.KeyValue{ + attribute.String("string", testString), + }, + } + + assert.Equal(t, want, spy.Record) + assert.Equal(t, ctx, spy.Context) +} diff --git a/log/benchmark/slog_test.go b/log/internal/slog.go similarity index 80% rename from log/benchmark/slog_test.go rename to log/internal/slog.go index 85e2470193c..347d1c8c6ea 100644 --- a/log/benchmark/slog_test.go +++ b/log/internal/slog.go @@ -1,42 +1,19 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package benchmark +package internal // import "go.opentelemetry.io/otel/log/internal" import ( "context" "fmt" "sync" - "testing" - "time" "go.opentelemetry.io/otel/attribute" "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, testBody, "string", testString) - - want := log.Record{ - Body: testBody, - Severity: log.SeverityInfo, - Attributes: []attribute.KeyValue{ - attribute.String("string", testString), - }, - } - - assert.NotZero(t, spy.Record.Timestamp, "should set a timestamp") - spy.Record.Timestamp = time.Time{} - assert.Equal(t, want, spy.Record) - assert.Equal(t, ctx, spy.Context) -} - type slogHandler struct { Logger log.Logger } diff --git a/log/internal/slog_test.go b/log/internal/slog_test.go new file mode 100644 index 00000000000..899bb3e4b65 --- /dev/null +++ b/log/internal/slog_test.go @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "testing" + "time" + + "go.opentelemetry.io/otel/attribute" + "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, testBody, "string", testString) + + want := log.Record{ + Body: testBody, + Severity: log.SeverityInfo, + Attributes: []attribute.KeyValue{ + attribute.String("string", testString), + }, + } + + assert.NotZero(t, spy.Record.Timestamp, "should set a timestamp") + spy.Record.Timestamp = time.Time{} + assert.Equal(t, want, spy.Record) + assert.Equal(t, ctx, spy.Context) +} diff --git a/log/benchmark/spy_test.go b/log/internal/spy_test.go similarity index 95% rename from log/benchmark/spy_test.go rename to log/internal/spy_test.go index 4170fa36ba3..c91682e7b23 100644 --- a/log/benchmark/spy_test.go +++ b/log/internal/spy_test.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package benchmark +package internal import ( "context" diff --git a/log/benchmark/writer_logger_test.go b/log/internal/writer_logger.go similarity index 70% rename from log/benchmark/writer_logger_test.go rename to log/internal/writer_logger.go index 3a0f1f1633a..7e21f3035b4 100644 --- a/log/benchmark/writer_logger_test.go +++ b/log/internal/writer_logger.go @@ -1,17 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package benchmark +package internal // import "go.opentelemetry.io/otel/log/internal" import ( "context" "fmt" "io" "strconv" - "strings" - "testing" - - "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" @@ -19,27 +15,6 @@ import ( "go.opentelemetry.io/otel/trace" ) -func TestWriterLogger(t *testing.T) { - sb := &strings.Builder{} - l := &writerLogger{w: sb} - - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - Attributes: []attribute.KeyValue{ - 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 traced=true\n" - assert.Equal(t, want, sb.String()) -} - // 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. diff --git a/log/internal/writer_logger_test.go b/log/internal/writer_logger_test.go new file mode 100644 index 00000000000..ab0dae929f7 --- /dev/null +++ b/log/internal/writer_logger_test.go @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +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, + Attributes: []attribute.KeyValue{ + 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 traced=true\n" + assert.Equal(t, want, sb.String()) +} diff --git a/versions.yaml b/versions.yaml index 8f98c0718d2..d257b9317b9 100644 --- a/versions.yaml +++ b/versions.yaml @@ -51,4 +51,4 @@ module-sets: excluded-modules: - go.opentelemetry.io/otel/internal/tools - go.opentelemetry.io/otel/log - - go.opentelemetry.io/otel/log/benchmark + - go.opentelemetry.io/otel/log/internal From a0bc4b8ad0bb5eb84268b2d00af0c2a8e0c3b8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 10 Jan 2024 14:13:04 +0100 Subject: [PATCH 55/83] Update log package --- log/doc.go | 82 +++++++++++++++++++++++++++++++++++++++++++- log/example_test.go | 8 ----- log/logger.go | 18 ++++++++-- log/provider.go | 26 +++++++++----- log/provider_test.go | 32 +++++++++++++++++ log/record.go | 22 +++++++++--- 6 files changed, 164 insertions(+), 24 deletions(-) delete mode 100644 log/example_test.go create mode 100644 log/provider_test.go diff --git a/log/doc.go b/log/doc.go index ad5db29756d..f9a9ec96a36 100644 --- a/log/doc.go +++ b/log/doc.go @@ -1,5 +1,85 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// Package log provides the OpenTelemetry Logs Data Model and Bridge API. +/* +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/example_test.go b/log/example_test.go deleted file mode 100644 index 209f5da17d3..00000000000 --- a/log/example_test.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package log_test - -func Example() { - // Output: -} diff --git a/log/logger.go b/log/logger.go index f75c9c9c9d0..9845b2754ff 100644 --- a/log/logger.go +++ b/log/logger.go @@ -9,10 +9,24 @@ import ( "go.opentelemetry.io/otel/log/embedded" ) -// Logger TODO: comment. +// 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 TODO: comment. + // Emit emits a log record. + // + // This method should: + // - be safe to call concurrently, + // - handle the trace context passed via ctx argument, + // - not modify the record's attributes, + // - copy the record's attributes in case of asynchronous processing, + // - use the current time as observed timestamp if the passed is empty. Emit(ctx context.Context, record Record) } diff --git a/log/provider.go b/log/provider.go index baa8a0d508b..ebb43e46ec2 100644 --- a/log/provider.go +++ b/log/provider.go @@ -8,11 +8,22 @@ import ( "go.opentelemetry.io/otel/log/embedded" ) -// LoggerProvider TODO: comment. +// 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 TODO: comment. + // 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 } @@ -43,26 +54,25 @@ func (cfg LoggerConfig) SchemaURL() string { return cfg.schemaURL } -// LoggerOption is an interface for applying Meter options. +// LoggerOption is an interface for applying Logger options. type LoggerOption interface { - // applyMeter is used to set a LoggerOption value of a LoggerConfig. - applyMeter(LoggerConfig) LoggerConfig + // 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. -// TODO: Add unit tests. func NewLoggerConfig(opts ...LoggerOption) LoggerConfig { var config LoggerConfig for _, o := range opts { - config = o.applyMeter(config) + config = o.applyLogger(config) } return config } type loggerOptionFunc func(LoggerConfig) LoggerConfig -func (fn loggerOptionFunc) applyMeter(cfg LoggerConfig) LoggerConfig { +func (fn loggerOptionFunc) applyLogger(cfg LoggerConfig) LoggerConfig { return fn(cfg) } 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 index d4e3d60c064..f4dccb37417 100644 --- a/log/record.go +++ b/log/record.go @@ -13,7 +13,7 @@ import ( "go.opentelemetry.io/otel/attribute" ) -// Record TODO: comment. +// Record represents a log record. type Record struct { Timestamp time.Time ObservedTimestamp time.Time @@ -23,32 +23,44 @@ type Record struct { Attributes []attribute.KeyValue } -// Severity TODO: comment. +// Severity represents a log record severity. +// 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 -// TODO: comment. +// Severity values defined by OpenTelemetry. const ( - SeverityUndefined Severity = iota - SeverityTrace + // A fine-grained debugging log record. Typically disabled in default configurations. + SeverityTrace Severity = iota + 1 SeverityTrace2 SeverityTrace3 SeverityTrace4 + + // A debugging log record. SeverityDebug SeverityDebug2 SeverityDebug3 SeverityDebug4 + + // An informational log record. Indicates that an event happened. SeverityInfo SeverityInfo2 SeverityInfo3 SeverityInfo4 + + // A warning log record. Not an error but is likely more important than an informational event. SeverityWarn SeverityWarn2 SeverityWarn3 SeverityWarn4 + + // An error log record. Something went wrong. SeverityError SeverityError2 SeverityError3 SeverityError4 + + // A fatal log record such as application or system crash. SeverityFatal SeverityFatal2 SeverityFatal3 From f6a230833c40b31fbc43ce11de367039013a7a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 10 Jan 2024 14:16:45 +0100 Subject: [PATCH 56/83] go mod tidy --- log/go.mod | 11 ++++++++++- log/go.sum | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/log/go.mod b/log/go.mod index abb4f8c45eb..121ff5f10f1 100644 --- a/log/go.mod +++ b/log/go.mod @@ -2,7 +2,16 @@ module go.opentelemetry.io/otel/log go 1.20 -require go.opentelemetry.io/otel v1.21.0 +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/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) replace go.opentelemetry.io/otel => ../ diff --git a/log/go.sum b/log/go.sum index 2d8ac4840ed..a6bcd03a15e 100644 --- a/log/go.sum +++ b/log/go.sum @@ -1,5 +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= From b7804723ca0b0b54c6ca1b21a7bcb42bc7be42d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 10 Jan 2024 15:22:10 +0100 Subject: [PATCH 57/83] Remove DESIGN.md --- log/DESIGN.md | 297 -------------------------------------------------- 1 file changed, 297 deletions(-) delete mode 100644 log/DESIGN.md diff --git a/log/DESIGN.md b/log/DESIGN.md deleted file mode 100644 index 6794abb1bc4..00000000000 --- a/log/DESIGN.md +++ /dev/null @@ -1,297 +0,0 @@ -# Logs Bridge API - -## Abstract - -`go.opentelemetry.io/otel/log` provides -[Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/). - -The initial version of the design and the prototype -was created in [#4725](https://github.com/open-telemetry/opentelemetry-go/pull/4725). - -## Background - -The key challenge is to create a well-performant API compliant with the specification. -Performance is seen as one of the most important characteristics of logging libraries in Go. - -## Design - -This proposed design aims to: - -- be specification compliant, -- have similar API to Trace and Metrics API, -- take advantage of both OpenTelemetry and `slog` experience to achieve acceptable performance. - -### Module structure - -The Go module consits of the following packages: - -- `go.opentelemetry.io/otel/log` -- `go.opentelemetry.io/otel/log/embedded` -- `go.opentelemetry.io/otel/log/noop` - -### LoggerProvider - -The [`LoggerProvider` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#loggerprovider) -is defined as an interface [provider.go](provider.go). - -### Logger - -The [`Logger` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) -is defined as an interface in [logger.go](logger.go). - -Canceling the context pass to `Emit` should not affect record processing. -Among other things, log messages may be necessary to debug a -cancellation-related problem. -The context is used to pass request-scoped values. -The API implementation should handle the trace context passed -in `ctx` to the `Emit` method. - -### Record - -The [`LogRecord` abstraction](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/#logger) -is defined as a struct in [record.go](record.go). - -## Usage examples - -### Log Bridge implementation - -The log bridges can use [`sync.Pool`](https://pkg.go.dev/sync#Pool) -for reducing the number of allocations when mapping attributes. - -The bridge implementation should do its best to pass -the `ctx` containing the trace context from the call-site -so it can later passed via `Emit`. -Re-constructing a `context.Context` with [`trace.ContextWithSpanContext`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#ContextWithSpanContext) -and [`trace.NewSpanContext`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#NewSpanContext) -would usually involve more memory allocations. - -The logging libraries which have recording methods that accepts `context.Context`, -such us [`slog`](https://pkg.go.dev/log/slog), -[`logrus`](https://pkg.go.dev/github.com/sirupsen/logrus) -[`zerolog`](https://pkg.go.dev/github.com/rs/zerolog), -makes passing the trace context trivial. - -However, some libraries do not accept a `context.Context` in their recording methods. -Structured logging libraries, -such as [`logr`](https://pkg.go.dev/github.com/go-logr/logr) -and [`zap`](https://pkg.go.dev/go.uber.org/zap), -offer passing `any` type as a log attribute/field. -Therefore, their bridge implementations can define a "special" log attributes/field -that will be used to capture the trace context. - -[The prototype](https://github.com/open-telemetry/opentelemetry-go/pull/4725) -has a naive implementation of -[slog.Handler](https://pkg.go.dev/log/slog#Handler) in `log/internal/slog.go` -and [logr.LogSink](https://pkg.go.dev/github.com/go-logr/logr#LogSink) in `log/internal/logr.go`. - -### Direct API usage - -The users may also chose to use the API directly. - -```go -package app - -var logger = otel.Logger("my-service") - -// In some function: -logger.Emit(ctx, Record{Severity: log.SeverityInfo, Body: "Application started."}) -``` - -### API implementation - -If the implementation processes the asynchronously, -then it has to copy record attributes, -in order to avoid use after free bugs and race condition. - -Excerpt of how SDK can implement the `Logger` interface. - -```go -type Logger struct { - scope instrumentation.Scope - processor Processor -} - -func (l *Logger) Emit(ctx context.Context, r log.Record) { - // Create log record model. - record, err := toModel(r) - if err != nil { - otel.Handle(err) - return - } - l.processor.Process(ctx, record) // Note: A batch processor copies the attributes. -} -``` - -A test implementation of the the `Logger` interface -used for benchmarking is in [internal/writer_logger.go](internal/writer_logger.go). - -## Compatibility - -The backwards compatibility is achieved using the `embedded` design pattern -that is already used in Trace API and Metrics API. - -Additionally, the `Logger.Emit` functionality can be extended by -adding new exported fields to the `Record` struct. - -## Benchmarking - -The benchmarks take inspiration from [`slog`](https://pkg.go.dev/log/slog), -because for the Go team it was also critical to create API that would be fast -and interoperable with existing logging packages.[^1][^2] - -## Rejected Alternatives - -### Reuse slog - -The API must not be coupled to [`slog`](https://pkg.go.dev/log/slog), -nor any other logging library. - -The API needs to evolve orthogonally to `slog`. - -`slog` is not compliant with the [Logs Bridge API](https://opentelemetry.io/docs/specs/otel/logs/bridge-api/). -and we cannot expect the Go team to make `slog` compliant with it. - -The interoperabilty can be achieved using [a log bridge](https://opentelemetry.io/docs/specs/otel/glossary/#log-appender--bridge). - -You can read more about OpenTelemetry Logs design on [opentelemetry.io](https://opentelemetry.io/docs/concepts/signals/logs/). - -### Record as interface - -`Record` is defined as a `struct` because of the following reasons. - -Log record is a value object without any behavior. -It is used as data input for Logger methods. - -The log record resembles the instrument config structs like [metric.Float64CounterConfig](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Float64CounterConfig). - -Using `struct` instead of `interface` should have better the performance as e.g. -indirect calls are less optimized, -usage of interfaces tend to increase heap allocations.[^2] - -The `Record` design is inspired by [`slog.Record`](https://pkg.go.dev/log/slog#Record). - -### Options as parameter to Logger.Emit - -One of the initial ideas was to have: - -```go -type Logger interface{ - embedded.Logger - Emit(ctx context.Context, options ...RecordOption) -} -``` - -The main reason was that design would be similar -to the [Meter API](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Meter) -for creating instruments. - -However, passing `Record` directly, instead of using options, -is more performant as it reduces heap allocations.[^3] - -Another advantage of passing `Record` is that API would not have functions like `NewRecord(options...)`, -which would be used by the SDK and not by the users. - -At last, the definition would be similar to [`slog.Handler.Handle`](https://pkg.go.dev/log/slog#Handler) -that was designed to provide optimization opportunities.[^1] - -### Passing record as pointer to Logger.Emit - -So far the benchmarks do not show differences that would -favor passing the record via pointer (and vice versa). - -Passing via value feels safer because of the following reasons. - -It follows the design of [`slog.Handler`](https://pkg.go.dev/log/slog#Handler). - -It should reduce the possibility of a heap allocation. - -The user would not be able to pass `nil`. -Therefore, it reduces the possiblity to have a nil pointer dereference. - -### Passing struct as parameter to LoggerProvider.Logger - -Similarly to `Logger.Emit`, we could have something like: - -```go -type Logger interface{ - embedded.Logger - Logger(name context.Context, config LoggerConfig) -} -``` - -The drawback of this idea would be that this would be -a different design from Trace and Metrics API. - -The performance of acquiring a logger is not as critical -as the performance of emitting a log record. While a single -HTTP/RPC handler could write hundreds of logs, it should not -create a new logger for each log entry. -The application should reuse loggers whenever possible. - -### Logger.WithAttributes - -We could add `WithAttributes` to the `Logger` interface. -Then `Record` could be a simple struct with only exported fields. -The idea was that the SDK would implement the performance improvements -instead of doing it in the API. -This would allow having different optimisation strategies. - -During the analysis[^4], it occurred that the main problem of this proposal -is that the variadic slice passed to an interface method is always heap allocated. - -Moreover, the logger returned by `WithAttribute` was allocated on the heap. - -At last, the proposal was not specification compliant. - -### Record attributes like in slog.Record - -To reduce the number of allocations of the attributes, -the `Record` could be modeled similarly to [`slog.Record`](https://pkg.go.dev/log/slog#Record). -`Record` could have `WalkAttributes` and `AddAttributes` methods, -like [`slog.Record.Attrs`](https://pkg.go.dev/log/slog#Record.Attrs) -and [`slog.Record.AddAttrs`](https://pkg.go.dev/log/slog#Record.AddAttrs), -in order to achieve high-performance when accessing and setting attributes efficiently. -`Record` would have a `AttributesLen` method that returns -the number of attributes to allow slice preallocation -when converting records to a different representation. - -However, during the analysis[^5] we decided that having -a simple slice in `Record` is more flexible. - -It is possible to achieve better performance, by using [`sync.Pool`](https://pkg.go.dev/sync#Pool). - -Having a simple `Record` without any logic makes it possible -that the optimisations can be done in API implementation -and bridge implementations. -For instance, in order to reduce the heap allocations of attributes, -the bridge implementation can use a `sync.Pool`. -In such case, the API implementation (SDK) would need to copy the attributes -when the records are processed asynchrounsly, -in order to avoid use after free bugs and race conditions. - -For reference, here is the reason why `slog` does not use `sync.Pool`[^2]: - -> We can use a sync pool for records though we decided not to. -You can but it's a bad idea for us. Why? -Because users have control of Records. -Handler writers can get their hands on a record -and we'd have to ask them to free it -or try to free it magically at some some point. -But either way, they could get themselves in trouble by freeing it twice -or holding on to one after they free it. -That's a use after free bug and that's why `zerolog` was problematic for us. -`zerolog` as as part of its speed exposes a pool allocated value to users -if you use `zerolog` the normal way, that you'll see in all the examples, -you will never encounter a problem. -But if you do something a little out of the ordinary you can get -use after free bugs and we just didn't want to put that in the standard library. - -We took a different decision, because the key difference is that `slog` -is a logging library and Logs Bridge API is only a logging abstraction. -We want to provide more flexibility and offer better speed. - -[^1]: Jonathan Amsterdam, [The Go Blog: Structured Logging with slog](https://go.dev/blog/slog) -[^2]: Jonathan Amsterdam, [GopherCon Europe 2023: A Fast Structured Logging Package](https://www.youtube.com/watch?v=tC4Jt3i62ns) -[^3]: [Emit definition discussion with benchmarks](https://github.com/open-telemetry/opentelemetry-go/pull/4725#discussion_r1400869566) -[^4]: [Logger.WithAttributes analysis](https://github.com/pellared/opentelemetry-go/pull/3) -[^5]: [Record attributes as field and use sync.Pool for reducing allocations analysis](https://github.com/pellared/opentelemetry-go/pull/4) From d8328174e66bf04bd08e2e8b4eae4be76769a306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 17 Jan 2024 08:48:04 +0100 Subject: [PATCH 58/83] Add Severity[Level]1 consts --- log/record.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/log/record.go b/log/record.go index f4dccb37417..81f1aff6c3e 100644 --- a/log/record.go +++ b/log/record.go @@ -31,38 +31,47 @@ type Severity int // Severity values defined by OpenTelemetry. const ( // A fine-grained debugging log record. Typically disabled in default configurations. - SeverityTrace Severity = iota + 1 + SeverityTrace1 Severity = iota + 1 SeverityTrace2 SeverityTrace3 SeverityTrace4 // A debugging log record. - SeverityDebug + SeverityDebug1 SeverityDebug2 SeverityDebug3 SeverityDebug4 // An informational log record. Indicates that an event happened. - SeverityInfo + SeverityInfo1 SeverityInfo2 SeverityInfo3 SeverityInfo4 // A warning log record. Not an error but is likely more important than an informational event. - SeverityWarn + SeverityWarn1 SeverityWarn2 SeverityWarn3 SeverityWarn4 // An error log record. Something went wrong. - SeverityError + SeverityError1 SeverityError2 SeverityError3 SeverityError4 // A fatal log record such as application or system crash. - SeverityFatal + SeverityFatal1 SeverityFatal2 SeverityFatal3 SeverityFatal4 ) + +const ( + SeverityTrace = SeverityTrace1 + SeverityDebug = SeverityDebug1 + SeverityInfo = SeverityInfo1 + SeverityWarn = SeverityWarn1 + SeverityError = SeverityError1 + SeverityFatal = SeverityFatal1 +) From f78cbec0d9821e427bd4e7b033214da398aa3596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 18 Jan 2024 10:38:04 +0100 Subject: [PATCH 59/83] Record attributes based on slog.Record (#6) --- log/go.mod | 4 + log/go.sum | 5 + log/internal/bench_test.go | 89 +++-------- log/internal/go.mod | 2 + log/internal/go.sum | 3 + log/internal/logr.go | 31 +--- log/internal/logr_test.go | 23 +-- log/internal/slog.go | 31 +--- log/internal/slog_test.go | 26 ++-- log/internal/writer_logger.go | 13 +- log/internal/writer_logger_test.go | 21 ++- log/record.go | 239 ++++++++++++++++++++++------- log/record_test.go | 178 +++++++++++++++++++++ log/severity.go | 59 +++++++ log/severity_test.go | 176 +++++++++++++++++++++ 15 files changed, 690 insertions(+), 210 deletions(-) create mode 100644 log/record_test.go create mode 100644 log/severity.go create mode 100644 log/severity_test.go diff --git a/log/go.mod b/log/go.mod index 121ff5f10f1..1e87877eaf9 100644 --- a/log/go.mod +++ b/log/go.mod @@ -9,7 +9,11 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/go.sum b/log/go.sum index a6bcd03a15e..75d8b1f55b4 100644 --- a/log/go.sum +++ b/log/go.sum @@ -1,5 +1,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/log/internal/bench_test.go b/log/internal/bench_test.go index b9a11ea1255..e3f8bc5abc0 100644 --- a/log/internal/bench_test.go +++ b/log/internal/bench_test.go @@ -14,7 +14,6 @@ package internal import ( "context" "io" - "sync" "testing" "time" @@ -45,13 +44,6 @@ var ( // slow logger implementation is skewing the results. The writerLogger // allocates memory only when using strconv. func BenchmarkEmit(b *testing.B) { - attrPool := sync.Pool{ - New: func() interface{} { - attr := make([]attribute.KeyValue, 0, 5) - return &attr - }, - } - for _, tc := range []struct { name string logger log.Logger @@ -67,34 +59,25 @@ func BenchmarkEmit(b *testing.B) { { "no attrs", func() { - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - } + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) tc.logger.Emit(ctx, r) }, }, { "3 attrs", func() { - ptr := attrPool.Get().(*[]attribute.KeyValue) - attrs := *ptr - defer func() { - *ptr = attrs[:0] - attrPool.Put(ptr) - }() - attrs = append(attrs, + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), ) - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - Attributes: attrs, - } tc.logger.Emit(ctx, r) }, }, @@ -105,38 +88,28 @@ func BenchmarkEmit(b *testing.B) { // should only be from strconv used in writerLogger. "5 attrs", func() { - ptr := attrPool.Get().(*[]attribute.KeyValue) - attrs := *ptr - defer func() { - *ptr = attrs[:0] - attrPool.Put(ptr) - }() - attrs = append(attrs, + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), attribute.Bool("bool", testBool), attribute.String("string", testString), ) - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - Attributes: attrs, - } tc.logger.Emit(ctx, r) }, }, { "10 attrs", func() { - ptr := attrPool.Get().(*[]attribute.KeyValue) - attrs := *ptr - defer func() { - *ptr = attrs[:0] - attrPool.Put(ptr) - }() - attrs = append(attrs, + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), @@ -148,25 +121,17 @@ func BenchmarkEmit(b *testing.B) { attribute.Bool("bool", testBool), attribute.String("string", testString), ) - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - Attributes: attrs, - } tc.logger.Emit(ctx, r) }, }, { "40 attrs", func() { - ptr := attrPool.Get().(*[]attribute.KeyValue) - attrs := *ptr - defer func() { - *ptr = attrs[:0] - attrPool.Put(ptr) - }() - attrs = append(attrs, + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(testBody) + r.AddAttributes( attribute.String("string", testString), attribute.Float64("float", testFloat), attribute.Int("int", testInt), @@ -208,12 +173,6 @@ func BenchmarkEmit(b *testing.B) { attribute.Bool("bool", testBool), attribute.String("string", testString), ) - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - Attributes: attrs, - } tc.logger.Emit(ctx, r) }, }, diff --git a/log/internal/go.mod b/log/internal/go.mod index a226081c490..a1226b609b9 100644 --- a/log/internal/go.mod +++ b/log/internal/go.mod @@ -13,7 +13,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // 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 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/internal/go.sum b/log/internal/go.sum index 949da0b69c3..7274ecbf3a5 100644 --- a/log/internal/go.sum +++ b/log/internal/go.sum @@ -1,7 +1,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/log/internal/logr.go b/log/internal/logr.go index 2a8654a43cc..81c82cfea35 100644 --- a/log/internal/logr.go +++ b/log/internal/logr.go @@ -6,7 +6,6 @@ package internal // import "go.opentelemetry.io/otel/log/internal" import ( "context" "fmt" - "sync" "github.com/go-logr/logr" @@ -18,18 +17,11 @@ type logrSink struct { Logger log.Logger } -var logrAttrPool = sync.Pool{ - New: func() interface{} { - attr := make([]attribute.KeyValue, 0, 5) - return &attr - }, -} - -// Init is implementated as a dummy. +// Init is implemented as a dummy. func (s *logrSink) Init(info logr.RuntimeInfo) { } -// Enabled is implementated as a dummy. +// Enabled is implemented as a dummy. func (s *logrSink) Enabled(level int) bool { return true } @@ -39,21 +31,15 @@ func (s *logrSink) Enabled(level int) bool { func (s *logrSink) Info(level int, msg string, keysAndValues ...any) { record := log.Record{} - record.Body = msg + record.SetBody(msg) lvl := log.Severity(9 - level) - record.Severity = lvl + record.SetSeverity(lvl) if len(keysAndValues)%2 == 1 { panic("key without a value") } kvCount := len(keysAndValues) / 2 - ptr := logrAttrPool.Get().(*[]attribute.KeyValue) - attrs := *ptr - defer func() { - *ptr = attrs[:0] - logrAttrPool.Put(ptr) - }() ctx := context.Background() for i := 0; i < kvCount; i++ { k, ok := keysAndValues[i*2].(string) @@ -67,23 +53,22 @@ func (s *logrSink) Info(level int, msg string, keysAndValues ...any) { continue } kv := convertKV(k, v) - attrs = append(attrs, kv) + record.AddAttributes(kv) } - record.Attributes = attrs s.Logger.Emit(ctx, record) } -// Error is implementated as a dummy. +// Error is implemented as a dummy. func (s *logrSink) Error(err error, msg string, keysAndValues ...any) { } -// WithValues is implementated as a dummy. +// WithValues is implemented as a dummy. func (s *logrSink) WithValues(keysAndValues ...any) logr.LogSink { return s } -// WithName is implementated as a dummy. +// WithName is implemented as a dummy. func (s *logrSink) WithName(name string) logr.LogSink { return s } diff --git a/log/internal/logr_test.go b/log/internal/logr_test.go index 3c1a3b5d371..0f628dfe857 100644 --- a/log/internal/logr_test.go +++ b/log/internal/logr_test.go @@ -20,14 +20,17 @@ func TestLogrSink(t *testing.T) { l.Info(testBody, "string", testString, "ctx", ctx) - want := log.Record{ - Body: testBody, - Severity: log.SeverityInfo, - Attributes: []attribute.KeyValue{ - attribute.String("string", testString), - }, - } - - assert.Equal(t, want, spy.Record) - assert.Equal(t, ctx, spy.Context) + want := log.Record{} + want.SetBody(testBody) + want.SetSeverity(log.SeverityInfo) + want.AddAttributes(attribute.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 attribute.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 index 347d1c8c6ea..db7ed3b4ff9 100644 --- a/log/internal/slog.go +++ b/log/internal/slog.go @@ -6,7 +6,6 @@ package internal // import "go.opentelemetry.io/otel/log/internal" import ( "context" "fmt" - "sync" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" @@ -18,52 +17,38 @@ type slogHandler struct { Logger log.Logger } -var slogAttrPool = sync.Pool{ - New: func() interface{} { - attr := make([]attribute.KeyValue, 0, 5) - return &attr - }, -} - // 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.Timestamp = r.Time + record.SetTimestamp(r.Time) - record.Body = r.Message + record.SetBody(r.Message) lvl := convertLevel(r.Level) - record.Severity = lvl - - ptr := slogAttrPool.Get().(*[]attribute.KeyValue) - attrs := *ptr - defer func() { - *ptr = attrs[:0] - slogAttrPool.Put(ptr) - }() + record.SetSeverity(lvl) + r.Attrs(func(a slog.Attr) bool { - attrs = append(attrs, convertAttr(a)) + record.AddAttributes(convertAttr(a)) return true }) - record.Attributes = attrs h.Logger.Emit(ctx, record) return nil } -// Enabled is implementated as a dummy. +// Enabled is implemented as a dummy. func (h *slogHandler) Enabled(_ context.Context, _ slog.Level) bool { return true } -// WithAttrs is implementated as a dummy. +// WithAttrs is implemented as a dummy. func (h *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h } -// WithGroup is implementated as a dummy. +// WithGroup is implemented as a dummy. func (h *slogHandler) WithGroup(name string) slog.Handler { return h } diff --git a/log/internal/slog_test.go b/log/internal/slog_test.go index 899bb3e4b65..8d1af2885b1 100644 --- a/log/internal/slog_test.go +++ b/log/internal/slog_test.go @@ -5,7 +5,6 @@ package internal import ( "testing" - "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" @@ -20,16 +19,17 @@ func TestSlogHandler(t *testing.T) { l.InfoContext(ctx, testBody, "string", testString) - want := log.Record{ - Body: testBody, - Severity: log.SeverityInfo, - Attributes: []attribute.KeyValue{ - attribute.String("string", testString), - }, - } - - assert.NotZero(t, spy.Record.Timestamp, "should set a timestamp") - spy.Record.Timestamp = time.Time{} - assert.Equal(t, want, spy.Record) - assert.Equal(t, ctx, spy.Context) + want := log.Record{} + want.SetBody(testBody) + want.SetSeverity(log.SeverityInfo) + want.AddAttributes(attribute.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 attribute.KeyValue) bool { + assert.Equal(t, "string", string(kv.Key)) + assert.Equal(t, testString, kv.Value.AsString()) + return true + }) } diff --git a/log/internal/writer_logger.go b/log/internal/writer_logger.go index 7e21f3035b4..3fc4b6e8b6f 100644 --- a/log/internal/writer_logger.go +++ b/log/internal/writer_logger.go @@ -24,22 +24,23 @@ type writerLogger struct { } func (l *writerLogger) Emit(ctx context.Context, r log.Record) { - if !r.Timestamp.IsZero() { + if !r.Timestamp().IsZero() { l.write("timestamp=") - l.write(strconv.FormatInt(r.Timestamp.Unix(), 10)) + l.write(strconv.FormatInt(r.Timestamp().Unix(), 10)) l.write(" ") } l.write("severity=") - l.write(strconv.FormatInt(int64(r.Severity), 10)) + l.write(strconv.FormatInt(int64(r.Severity()), 10)) l.write(" ") l.write("body=") - l.write(r.Body) - for _, kv := range r.Attributes { + l.write(r.Body()) + r.WalkAttributes(func(kv attribute.KeyValue) bool { l.write(" ") l.write(string(kv.Key)) l.write("=") l.appendValue(kv.Value) - } + return true + }) span := trace.SpanContextFromContext(ctx) if span.IsValid() { diff --git a/log/internal/writer_logger_test.go b/log/internal/writer_logger_test.go index ab0dae929f7..af0637c88bd 100644 --- a/log/internal/writer_logger_test.go +++ b/log/internal/writer_logger_test.go @@ -17,17 +17,16 @@ func TestWriterLogger(t *testing.T) { sb := &strings.Builder{} l := &writerLogger{w: sb} - r := log.Record{ - Timestamp: testTimestamp, - Severity: testSeverity, - Body: testBody, - Attributes: []attribute.KeyValue{ - attribute.String("string", testString), - attribute.Float64("float", testFloat), - attribute.Int("int", testInt), - attribute.Bool("bool", testBool), - }, - } + r := log.Record{} + r.SetTimestamp(testTimestamp) + r.SetSeverity(testSeverity) + r.SetBody(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 traced=true\n" diff --git a/log/record.go b/log/record.go index 81f1aff6c3e..bbbca2fc257 100644 --- a/log/record.go +++ b/log/record.go @@ -8,70 +8,191 @@ package log // import "go.opentelemetry.io/otel/log" import ( + "errors" "time" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) // Record represents a log record. type Record struct { - Timestamp time.Time - ObservedTimestamp time.Time - Severity Severity - SeverityText string - Body string - Attributes []attribute.KeyValue -} - -// Severity represents a log record severity. -// 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 = iota + 1 - SeverityTrace2 - SeverityTrace3 - SeverityTrace4 - - // A debugging log record. - SeverityDebug1 - SeverityDebug2 - SeverityDebug3 - SeverityDebug4 - - // An informational log record. Indicates that an event happened. - SeverityInfo1 - SeverityInfo2 - SeverityInfo3 - SeverityInfo4 - - // A warning log record. Not an error but is likely more important than an informational event. - SeverityWarn1 - SeverityWarn2 - SeverityWarn3 - SeverityWarn4 - - // An error log record. Something went wrong. - SeverityError1 - SeverityError2 - SeverityError3 - SeverityError4 - - // A fatal log record such as application or system crash. - SeverityFatal1 - SeverityFatal2 - SeverityFatal3 - SeverityFatal4 -) + timestamp time.Time + observedTimestamp time.Time + severity Severity + severityText string + body string -const ( - SeverityTrace = SeverityTrace1 - SeverityDebug = SeverityDebug1 - SeverityInfo = SeverityInfo1 - SeverityWarn = SeverityWarn1 - SeverityError = SeverityError1 - SeverityFatal = SeverityFatal1 -) + // 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]attribute.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 []attribute.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 value containing the body of the log record +// as a human-readable string message (including multi-line) +// describing the log record. +func (r Record) Body() string { + return r.body +} + +// SetBody sets the value containing the body of the log record +// as a human-readable string message (including multi-line) +// describing the log record. +func (r *Record) SetBody(s string) { + r.body = s +} + +// WalkAttributes calls f on each [attribute.KeyValue] in the [Record]. +// Iteration stops if f returns false. +func (r Record) WalkAttributes(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 + } + } +} + +var errUnsafeAddAttrs = errors.New("unsafely called AddAttributes on copy of Record made without using Record.Clone") + +// 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 attribute 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 = sliceClip(r.back) + otel.Handle(errUnsafeAddAttrs) + } + } + ne := countInvalidAttrs(attrs[i:]) + r.back = sliceGrow(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 = sliceClip(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 +} + +// 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 +} + +// sliceClip removes unused capacity from the slice, returning s[:len(s):len(s)]. +// +// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. +func sliceClip[S ~[]E, E any](s S) S { + return s[:len(s):len(s)] +} diff --git a/log/record_test.go b/log/record_test.go new file mode 100644 index 00000000000..89cec8f97ed --- /dev/null +++ b/log/record_test.go @@ -0,0 +1,178 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) + +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{} + + r.SetBody(testString) + + assert.Equal(t, testString, r.Body()) +} + +func TestRecordAttributes(t *testing.T) { + r := Record{} + attrs := []attribute.KeyValue{ + attribute.String("k1", testString), + attribute.Float64("k2", testFloat), + attribute.Int("k3", testInt), + attribute.Bool("k4", testBool), + attribute.String("k5", testString), + attribute.Float64("k6", testFloat), + attribute.Int("k7", testInt), + attribute.Bool("k8", testBool), + } + r.AddAttributes(attrs...) + + assert.Equal(t, len(attrs), r.AttributesLen()) + + var got []attribute.KeyValue + r.WalkAttributes(func(kv attribute.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 attribute.KeyValue) bool { + i++ + return i < tc.index + }) + assert.Equal(t, tc.index, i, "WalkAttributes early return for %s", tc.name) + } +} + +func TestRecordAttributesInvalid(t *testing.T) { + r := Record{} + attrs := []attribute.KeyValue{ + attribute.String("k1", testString), + {}, + attribute.Int("k3", testInt), + attribute.Bool("k4", testBool), + attribute.String("k5", testString), + attribute.Float64("k6", testFloat), + attribute.Int("k7", testInt), + {}, + } + r.AddAttributes(attrs...) + + assert.Equal(t, len(attrs)-2, r.AttributesLen()) +} + +func TestRecordAliasingAndClone(t *testing.T) { + defer func(orig otel.ErrorHandler) { + otel.SetErrorHandler(orig) + }(otel.GetErrorHandler()) + var errs []error + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { + errs = append(errs, err) + })) + + // Create a record whose Attrs overflow the inline array, + // creating a slice in r.back. + r1 := Record{} + for i := 0; i < attributesInlineCount+1; i++ { + r1.AddAttributes(attribute.Int("k", i)) + } + + // Ensure that r1.back's capacity exceeds its length. + b := make([]attribute.KeyValue, len(r1.back), len(r1.back)+1) + copy(b, r1.back) + r1.back = b + + // Make a copy that shares state. + // Adding to both should emit an special error for the second call. + r2 := r1 + r1AttrsBefore := attrsSlice(r1) + r1.AddAttributes(attribute.Int("p", 0)) + assert.Zero(t, errs) + r2.AddAttributes(attribute.Int("p", 1)) + assert.Equal(t, []error{errUnsafeAddAttrs}, errs, "sends an error via ErrorHandler when a dirty AddAttribute is detected") + errs = nil + assert.Equal(t, append(r1AttrsBefore, attribute.Int("p", 0)), attrsSlice(r1)) + assert.Equal(t, append(r1AttrsBefore, attribute.Int("p", 1)), attrsSlice(r2)) + + // Adding to a clone is fine. + r1Attrs := attrsSlice(r1) + r3 := r1.Clone() + assert.Equal(t, r1Attrs, attrsSlice(r3)) + r3.AddAttributes(attribute.Int("p", 2)) + assert.Zero(t, errs) + assert.Equal(t, r1Attrs, attrsSlice(r1), "r1 is unchanged") + assert.Equal(t, append(r1Attrs, attribute.Int("p", 2)), attrsSlice(r3)) +} + +func attrsSlice(r Record) []attribute.KeyValue { + var attrs []attribute.KeyValue + r.WalkAttributes(func(kv attribute.KeyValue) bool { + attrs = append(attrs, kv) + return true + }) + return attrs +} diff --git a/log/severity.go b/log/severity.go new file mode 100644 index 00000000000..c9076457be1 --- /dev/null +++ b/log/severity.go @@ -0,0 +1,59 @@ +// 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" + +// 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 = iota + 1 + SeverityTrace2 + SeverityTrace3 + SeverityTrace4 + + // A debugging log record. + SeverityDebug1 + SeverityDebug2 + SeverityDebug3 + SeverityDebug4 + + // An informational log record. Indicates that an event happened. + SeverityInfo1 + SeverityInfo2 + SeverityInfo3 + SeverityInfo4 + + // A warning log record. Not an error but is likely more important than an informational event. + SeverityWarn1 + SeverityWarn2 + SeverityWarn3 + SeverityWarn4 + + // An error log record. Something went wrong. + SeverityError1 + SeverityError2 + SeverityError3 + SeverityError4 + + // A fatal log record such as application or system crash. + SeverityFatal1 + SeverityFatal2 + SeverityFatal3 + SeverityFatal4 + + SeverityTrace = SeverityTrace1 + SeverityDebug = SeverityDebug1 + SeverityInfo = SeverityInfo1 + SeverityWarn = SeverityWarn1 + SeverityError = SeverityError1 + SeverityFatal = SeverityFatal1 +) diff --git a/log/severity_test.go b/log/severity_test.go new file mode 100644 index 00000000000..90dfde04f02 --- /dev/null +++ b/log/severity_test.go @@ -0,0 +1,176 @@ +// 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 + }{ + { + name: "SeverityTrace", + severity: log.SeverityTrace, + value: 1, + }, + { + name: "SeverityTrace1", + severity: log.SeverityTrace1, + value: 1, + }, + { + name: "SeverityTrace2", + severity: log.SeverityTrace2, + value: 2, + }, + { + name: "SeverityTrace3", + severity: log.SeverityTrace3, + value: 3, + }, + { + name: "SeverityTrace4", + severity: log.SeverityTrace4, + value: 4, + }, + { + name: "SeverityDebug", + severity: log.SeverityDebug, + value: 5, + }, + { + name: "SeverityDebug1", + severity: log.SeverityDebug1, + value: 5, + }, + { + name: "SeverityDebug2", + severity: log.SeverityDebug2, + value: 6, + }, + { + name: "SeverityDebug3", + severity: log.SeverityDebug3, + value: 7, + }, + { + name: "SeverityDebug4", + severity: log.SeverityDebug4, + value: 8, + }, + { + name: "SeverityInfo", + severity: log.SeverityInfo, + value: 9, + }, + { + name: "SeverityInfo1", + severity: log.SeverityInfo1, + value: 9, + }, + { + name: "SeverityInfo2", + severity: log.SeverityInfo2, + value: 10, + }, + { + name: "SeverityInfo3", + severity: log.SeverityInfo3, + value: 11, + }, + { + name: "SeverityInfo4", + severity: log.SeverityInfo4, + value: 12, + }, + { + name: "SeverityWarn", + severity: log.SeverityWarn, + value: 13, + }, + { + name: "SeverityWarn1", + severity: log.SeverityWarn1, + value: 13, + }, + { + name: "SeverityWarn2", + severity: log.SeverityWarn2, + value: 14, + }, + { + name: "SeverityWarn3", + severity: log.SeverityWarn3, + value: 15, + }, + { + name: "SeverityWarn4", + severity: log.SeverityWarn4, + value: 16, + }, + { + name: "SeverityError", + severity: log.SeverityError, + value: 17, + }, + { + name: "SeverityError1", + severity: log.SeverityError1, + value: 17, + }, + { + name: "SeverityError2", + severity: log.SeverityError2, + value: 18, + }, + { + name: "SeverityError3", + severity: log.SeverityError3, + value: 19, + }, + { + name: "SeverityError4", + severity: log.SeverityError4, + value: 20, + }, + { + name: "SeverityFatal", + severity: log.SeverityFatal, + value: 21, + }, + { + name: "SeverityFatal1", + severity: log.SeverityFatal1, + value: 21, + }, + { + name: "SeverityFatal2", + severity: log.SeverityFatal2, + value: 22, + }, + { + name: "SeverityFatal3", + severity: log.SeverityFatal3, + value: 23, + }, + { + name: "SeverityFatal4", + severity: log.SeverityFatal4, + value: 24, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.value, int(tc.severity)) + }) + } +} From 6730f06a49980160e1f3d1687d050d71d42be79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 18 Jan 2024 10:45:27 +0100 Subject: [PATCH 60/83] go mod tidy --- log/go.mod | 6 +++--- log/internal/go.mod | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/log/go.mod b/log/go.mod index 1e87877eaf9..74e5038b67f 100644 --- a/log/go.mod +++ b/log/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel v1.22.0 ) require ( @@ -12,8 +12,8 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/internal/go.mod b/log/internal/go.mod index a1226b609b9..f6e9cfdf0d0 100644 --- a/log/internal/go.mod +++ b/log/internal/go.mod @@ -5,9 +5,9 @@ go 1.20 require ( github.com/go-logr/logr v1.4.1 github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 - go.opentelemetry.io/otel/trace v1.21.0 + go.opentelemetry.io/otel/trace v1.22.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e ) @@ -15,7 +15,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // 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/metric v1.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From d1144643127dd9ef602e623b092bf9a76ade3512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 18 Jan 2024 21:17:18 +0100 Subject: [PATCH 61/83] The caller must not subsequently mutate the record passed to Emit --- log/logger.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/log/logger.go b/log/logger.go index 9845b2754ff..865ec29fddd 100644 --- a/log/logger.go +++ b/log/logger.go @@ -21,12 +21,11 @@ type Logger interface { embedded.Logger // Emit emits a log record. + // The caller must not subsequently mutate the record. // // This method should: // - be safe to call concurrently, // - handle the trace context passed via ctx argument, - // - not modify the record's attributes, - // - copy the record's attributes in case of asynchronous processing, // - use the current time as observed timestamp if the passed is empty. Emit(ctx context.Context, record Record) } From f08ebbf030139ffd6bce9545ceee735ffe118cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 18 Jan 2024 21:33:36 +0100 Subject: [PATCH 62/83] Refine Emit docs --- log/logger.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/log/logger.go b/log/logger.go index 865ec29fddd..84333af8672 100644 --- a/log/logger.go +++ b/log/logger.go @@ -25,7 +25,8 @@ type Logger interface { // // This method should: // - be safe to call concurrently, - // - handle the trace context passed via ctx argument, - // - use the current time as observed timestamp if the passed is empty. + // - 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) } From a1166091e157d01ab5d5ffc536b6921ce2708b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 18 Jan 2024 21:38:21 +0100 Subject: [PATCH 63/83] go mod tidy --- log/go.mod | 6 +++--- log/internal/go.mod | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/log/go.mod b/log/go.mod index 74e5038b67f..3072098ad4a 100644 --- a/log/go.mod +++ b/log/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel v1.23.0-rc.1 ) require ( @@ -12,8 +12,8 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect - go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0-rc.1 // indirect + go.opentelemetry.io/otel/trace v1.23.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/internal/go.mod b/log/internal/go.mod index f6e9cfdf0d0..c84b8352645 100644 --- a/log/internal/go.mod +++ b/log/internal/go.mod @@ -5,9 +5,9 @@ go 1.20 require ( github.com/go-logr/logr v1.4.1 github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel v1.23.0-rc.1 go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 - go.opentelemetry.io/otel/trace v1.22.0 + go.opentelemetry.io/otel/trace v1.23.0-rc.1 golang.org/x/exp v0.0.0-20231127185646-65229373498e ) @@ -15,7 +15,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // 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.22.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From 5d8c58f34144b0b4c02494521b284ae3d9623283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 24 Jan 2024 15:25:54 +0100 Subject: [PATCH 64/83] Handle structured body and attributes (#7) --- log/internal/bench_test.go | 154 ++++++------ log/internal/go.mod | 2 +- log/internal/logr.go | 13 +- log/internal/logr_test.go | 9 +- log/internal/slog.go | 25 +- log/internal/slog_test.go | 9 +- log/internal/writer_logger.go | 39 +-- log/internal/writer_logger_test.go | 9 +- log/record.go | 60 ++--- log/record_test.go | 70 +++--- log/severity.go | 4 - log/slice.go | 48 ++++ log/value.go | 370 +++++++++++++++++++++++++++++ log/value_test.go | 191 +++++++++++++++ 14 files changed, 796 insertions(+), 207 deletions(-) create mode 100644 log/slice.go create mode 100644 log/value.go create mode 100644 log/value_test.go diff --git a/log/internal/bench_test.go b/log/internal/bench_test.go index e3f8bc5abc0..f3fd4a86f67 100644 --- a/log/internal/bench_test.go +++ b/log/internal/bench_test.go @@ -17,7 +17,6 @@ import ( "testing" "time" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/noop" "go.opentelemetry.io/otel/trace" @@ -27,14 +26,15 @@ import ( ) 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) - testBody = "log message" - testSeverity = log.SeverityInfo - testFloat = 1.2345 - testString = "7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190" - testInt = 32768 - testBool = true + 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 @@ -74,9 +74,9 @@ func BenchmarkEmit(b *testing.B) { r.SetSeverity(testSeverity) r.SetBody(testBody) r.AddAttributes( - attribute.String("string", testString), - attribute.Float64("float", testFloat), - attribute.Int("int", testInt), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), ) tc.logger.Emit(ctx, r) }, @@ -93,11 +93,11 @@ func BenchmarkEmit(b *testing.B) { r.SetSeverity(testSeverity) r.SetBody(testBody) r.AddAttributes( - attribute.String("string", testString), - attribute.Float64("float", testFloat), - attribute.Int("int", testInt), - attribute.Bool("bool", testBool), - attribute.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) }, @@ -110,16 +110,16 @@ func BenchmarkEmit(b *testing.B) { r.SetSeverity(testSeverity) r.SetBody(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), + 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) }, @@ -132,46 +132,46 @@ func BenchmarkEmit(b *testing.B) { r.SetSeverity(testSeverity) r.SetBody(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), + 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) }, @@ -199,13 +199,13 @@ func BenchmarkSlog(b *testing.B) { { "no attrs", func() { - logger.LogAttrs(ctx, slog.LevelInfo, testBody) + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString) }, }, { "3 attrs", func() { - logger.LogAttrs(ctx, slog.LevelInfo, testBody, + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, slog.String("string", testString), slog.Float64("float", testFloat), slog.Int("int", testInt), @@ -215,7 +215,7 @@ func BenchmarkSlog(b *testing.B) { { "5 attrs", func() { - logger.LogAttrs(ctx, slog.LevelInfo, testBody, + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, slog.String("string", testString), slog.Float64("float", testFloat), slog.Int("int", testInt), @@ -227,7 +227,7 @@ func BenchmarkSlog(b *testing.B) { { "10 attrs", func() { - logger.LogAttrs(ctx, slog.LevelInfo, testBody, + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, slog.String("string", testString), slog.Float64("float", testFloat), slog.Int("int", testInt), @@ -244,7 +244,7 @@ func BenchmarkSlog(b *testing.B) { { "40 attrs", func() { - logger.LogAttrs(ctx, slog.LevelInfo, testBody, + logger.LogAttrs(ctx, slog.LevelInfo, testBodyString, slog.String("string", testString), slog.Float64("float", testFloat), slog.Int("int", testInt), @@ -309,13 +309,13 @@ func BenchmarkLogr(b *testing.B) { { "no attrs", func() { - logger.Info(testBody) + logger.Info(testBodyString) }, }, { "3 attrs", func() { - logger.Info(testBody, + logger.Info(testBodyString, "string", testString, "float", testFloat, "int", testInt, @@ -329,7 +329,7 @@ func BenchmarkLogr(b *testing.B) { // should only be from strconv used in writerLogger. "5 attrs", func() { - logger.Info(testBody, + logger.Info(testBodyString, "string", testString, "float", testFloat, "int", testInt, @@ -341,7 +341,7 @@ func BenchmarkLogr(b *testing.B) { { "10 attrs", func() { - logger.Info(testBody, + logger.Info(testBodyString, "string", testString, "float", testFloat, "int", testInt, @@ -358,7 +358,7 @@ func BenchmarkLogr(b *testing.B) { { "40 attrs", func() { - logger.Info(testBody, + logger.Info(testBodyString, "string", testString, "float", testFloat, "int", testInt, diff --git a/log/internal/go.mod b/log/internal/go.mod index c84b8352645..5680a1f3aac 100644 --- a/log/internal/go.mod +++ b/log/internal/go.mod @@ -5,7 +5,6 @@ go 1.20 require ( github.com/go-logr/logr v1.4.1 github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.23.0-rc.1 go.opentelemetry.io/otel/log v0.0.0-00010101000000-000000000000 go.opentelemetry.io/otel/trace v1.23.0-rc.1 golang.org/x/exp v0.0.0-20231127185646-65229373498e @@ -15,6 +14,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel v1.23.0-rc.1 // indirect go.opentelemetry.io/otel/metric v1.23.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/internal/logr.go b/log/internal/logr.go index 81c82cfea35..43c62da3c11 100644 --- a/log/internal/logr.go +++ b/log/internal/logr.go @@ -9,7 +9,6 @@ import ( "github.com/go-logr/logr" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) @@ -31,7 +30,7 @@ func (s *logrSink) Enabled(level int) bool { func (s *logrSink) Info(level int, msg string, keysAndValues ...any) { record := log.Record{} - record.SetBody(msg) + record.SetBody(log.StringValue(msg)) lvl := log.Severity(9 - level) record.SetSeverity(lvl) @@ -73,16 +72,16 @@ func (s *logrSink) WithName(name string) logr.LogSink { return s } -func convertKV(k string, v interface{}) attribute.KeyValue { +func convertKV(k string, v interface{}) log.KeyValue { switch val := v.(type) { case bool: - return attribute.Bool(k, val) + return log.Bool(k, val) case float64: - return attribute.Float64(k, val) + return log.Float64(k, val) case int: - return attribute.Int(k, val) + return log.Int(k, val) case string: - return attribute.String(k, val) + 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 index 0f628dfe857..ba5620bf572 100644 --- a/log/internal/logr_test.go +++ b/log/internal/logr_test.go @@ -9,7 +9,6 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) @@ -18,19 +17,19 @@ func TestLogrSink(t *testing.T) { l := logr.New(&logrSink{spy}) - l.Info(testBody, "string", testString, "ctx", ctx) + l.Info(testBodyString, "string", testString, "ctx", ctx) want := log.Record{} want.SetBody(testBody) want.SetSeverity(log.SeverityInfo) - want.AddAttributes(attribute.String("string", testString)) + 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 attribute.KeyValue) bool { + spy.Record.WalkAttributes(func(kv log.KeyValue) bool { assert.Equal(t, "string", string(kv.Key)) - assert.Equal(t, testString, kv.Value.AsString()) + assert.Equal(t, testString, kv.Value.String()) return true }) } diff --git a/log/internal/slog.go b/log/internal/slog.go index db7ed3b4ff9..b6104e3576b 100644 --- a/log/internal/slog.go +++ b/log/internal/slog.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "golang.org/x/exp/slog" @@ -24,7 +23,7 @@ func (h *slogHandler) Handle(ctx context.Context, r slog.Record) error { record.SetTimestamp(r.Time) - record.SetBody(r.Message) + record.SetBody(log.StringValue(r.Message)) lvl := convertLevel(r.Level) record.SetSeverity(lvl) @@ -57,29 +56,29 @@ func convertLevel(l slog.Level) log.Severity { return log.Severity(l + 9) } -func convertAttr(attr slog.Attr) attribute.KeyValue { +func convertAttr(attr slog.Attr) log.KeyValue { val := convertValue(attr.Value) - return attribute.KeyValue{Key: attribute.Key(attr.Key), Value: val} + return log.KeyValue{Key: attr.Key, Value: val} } -func convertValue(v slog.Value) attribute.Value { +func convertValue(v slog.Value) log.Value { switch v.Kind() { case slog.KindAny: - return attribute.StringValue(fmt.Sprintf("%+v", v.Any())) + return log.StringValue(fmt.Sprintf("%+v", v.Any())) case slog.KindBool: - return attribute.BoolValue(v.Bool()) + return log.BoolValue(v.Bool()) case slog.KindDuration: - return attribute.Int64Value(v.Duration().Nanoseconds()) + return log.Int64Value(v.Duration().Nanoseconds()) case slog.KindFloat64: - return attribute.Float64Value(v.Float64()) + return log.Float64Value(v.Float64()) case slog.KindInt64: - return attribute.Int64Value(v.Int64()) + return log.Int64Value(v.Int64()) case slog.KindString: - return attribute.StringValue(v.String()) + return log.StringValue(v.String()) case slog.KindTime: - return attribute.Int64Value(v.Time().UnixNano()) + return log.Int64Value(v.Time().UnixNano()) case slog.KindUint64: - return attribute.Int64Value(int64(v.Uint64())) + 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 index 8d1af2885b1..0b037dca6a4 100644 --- a/log/internal/slog_test.go +++ b/log/internal/slog_test.go @@ -6,7 +6,6 @@ package internal import ( "testing" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "github.com/stretchr/testify/assert" @@ -17,19 +16,19 @@ func TestSlogHandler(t *testing.T) { spy := &spyLogger{} l := slog.New(&slogHandler{spy}) - l.InfoContext(ctx, testBody, "string", testString) + l.InfoContext(ctx, testBodyString, "string", testString) want := log.Record{} want.SetBody(testBody) want.SetSeverity(log.SeverityInfo) - want.AddAttributes(attribute.String("string", testString)) + 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 attribute.KeyValue) bool { + spy.Record.WalkAttributes(func(kv log.KeyValue) bool { assert.Equal(t, "string", string(kv.Key)) - assert.Equal(t, testString, kv.Value.AsString()) + assert.Equal(t, testString, kv.Value.String()) return true }) } diff --git a/log/internal/writer_logger.go b/log/internal/writer_logger.go index 3fc4b6e8b6f..fcbc87ccb73 100644 --- a/log/internal/writer_logger.go +++ b/log/internal/writer_logger.go @@ -9,7 +9,6 @@ import ( "io" "strconv" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" "go.opentelemetry.io/otel/trace" @@ -32,9 +31,13 @@ func (l *writerLogger) Emit(ctx context.Context, r log.Record) { l.write("severity=") l.write(strconv.FormatInt(int64(r.Severity()), 10)) l.write(" ") - l.write("body=") - l.write(r.Body()) - r.WalkAttributes(func(kv attribute.KeyValue) bool { + + 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("=") @@ -50,18 +53,24 @@ func (l *writerLogger) Emit(ctx context.Context, r log.Record) { 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())) +func (l *writerLogger) appendValue(v log.Value) { + switch v.Kind() { + case log.KindString: + l.write(v.String()) + case log.KindInt64: + l.write(strconv.FormatInt(v.Int64(), 10)) // strconv.FormatInt allocates memory. + case log.KindFloat64: + l.write(strconv.FormatFloat(v.Float64(), 'g', -1, 64)) // strconv.FormatFloat allocates memory. + case log.KindBool: + l.write(strconv.FormatBool(v.Bool())) + case log.KindBytes: + l.write(fmt.Sprint(v.Bytes())) + case log.KindMap: + l.write(fmt.Sprint(v.Map())) + case log.KindEmpty: + l.write("") default: - panic(fmt.Sprintf("unhandled attribute type: %s", v.Type())) + panic(fmt.Sprintf("unhandled value kind: %s", v.Kind())) } } diff --git a/log/internal/writer_logger_test.go b/log/internal/writer_logger_test.go index af0637c88bd..9abcc6ccd1b 100644 --- a/log/internal/writer_logger_test.go +++ b/log/internal/writer_logger_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) @@ -22,10 +21,10 @@ func TestWriterLogger(t *testing.T) { r.SetSeverity(testSeverity) r.SetBody(testBody) r.AddAttributes( - attribute.String("string", testString), - attribute.Float64("float", testFloat), - attribute.Int("int", testInt), - attribute.Bool("bool", testBool), + log.String("string", testString), + log.Float64("float", testFloat), + log.Int("int", testInt), + log.Bool("bool", testBool), ) l.Emit(ctx, r) diff --git a/log/record.go b/log/record.go index bbbca2fc257..0afb9600599 100644 --- a/log/record.go +++ b/log/record.go @@ -12,7 +12,6 @@ import ( "time" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" ) // Record represents a log record. @@ -21,7 +20,7 @@ type Record struct { observedTimestamp time.Time severity Severity severityText string - body string + body Value // The fields below are for optimizing the implementation of // Attributes and AddAttributes. @@ -29,7 +28,7 @@ type Record struct { // 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]attribute.KeyValue + front [attributesInlineCount]KeyValue // The number of attributes in front. nFront int @@ -38,7 +37,7 @@ type Record struct { // Invariants: // - len(back) > 0 if nFront == len(front) // - Unused array elements are zero. Used to detect mistakes. - back []attribute.KeyValue + back []KeyValue } const attributesInlineCount = 5 @@ -90,23 +89,19 @@ func (r *Record) SetSeverityText(s string) { r.severityText = s } -// Body returns the value containing the body of the log record -// as a human-readable string message (including multi-line) -// describing the log record. -func (r Record) Body() string { +// Body returns the the body of the log record as a strucutured value. +func (r Record) Body() Value { return r.body } -// SetBody sets the value containing the body of the log record -// as a human-readable string message (including multi-line) -// describing the log record. -func (r *Record) SetBody(s string) { - r.body = s +// 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 [attribute.KeyValue] in the [Record]. +// WalkAttributes calls f on each [KeyValue] in the [Record]. // Iteration stops if f returns false. -func (r Record) WalkAttributes(f func(attribute.KeyValue) bool) { +func (r Record) WalkAttributes(f func(KeyValue) bool) { for i := 0; i < r.nFront; i++ { if !f(r.front[i]) { return @@ -124,11 +119,11 @@ var errUnsafeAddAttrs = errors.New("unsafely called AddAttributes on copy of Rec // 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) { +func (r *Record) AddAttributes(attrs ...KeyValue) { var i int for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ { a := attrs[i] - if !a.Valid() { + if a.Invalid() { continue } r.front[r.nFront] = a @@ -138,7 +133,7 @@ func (r *Record) AddAttributes(attrs ...attribute.KeyValue) { // and seeing if the attribute there is non-zero. if cap(r.back) > len(r.back) { end := r.back[:len(r.back)+1][len(r.back)] - if end.Valid() { + if !end.Invalid() { // Don't panic; copy and muddle through. r.back = sliceClip(r.back) otel.Handle(errUnsafeAddAttrs) @@ -147,9 +142,10 @@ func (r *Record) AddAttributes(attrs ...attribute.KeyValue) { ne := countInvalidAttrs(attrs[i:]) r.back = sliceGrow(r.back, len(attrs[i:])-ne) for _, a := range attrs[i:] { - if a.Valid() { - r.back = append(r.back, a) + if a.Invalid() { + continue } + r.back = append(r.back, a) } } @@ -167,32 +163,12 @@ func (r Record) AttributesLen() int { } // countInvalidAttrs returns the number of invalid attributes. -func countInvalidAttrs(as []attribute.KeyValue) int { +func countInvalidAttrs(as []KeyValue) int { n := 0 for _, a := range as { - if !a.Valid() { + if a.Invalid() { n++ } } return n } - -// 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 -} - -// sliceClip removes unused capacity from the slice, returning s[:len(s):len(s)]. -// -// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. -func sliceClip[S ~[]E, E any](s S) S { - return s[:len(s):len(s)] -} diff --git a/log/record_test.go b/log/record_test.go index 89cec8f97ed..0217c46baaf 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -1,6 +1,10 @@ // 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 ( @@ -10,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" ) var ( @@ -56,30 +59,31 @@ func TestRecordSeverityText(t *testing.T) { func TestRecordBody(t *testing.T) { r := Record{} + body := StringValue(testString) - r.SetBody(testString) + r.SetBody(body) - assert.Equal(t, testString, r.Body()) + assert.Equal(t, body, r.Body()) } func TestRecordAttributes(t *testing.T) { r := Record{} - attrs := []attribute.KeyValue{ - attribute.String("k1", testString), - attribute.Float64("k2", testFloat), - attribute.Int("k3", testInt), - attribute.Bool("k4", testBool), - attribute.String("k5", testString), - attribute.Float64("k6", testFloat), - attribute.Int("k7", testInt), - attribute.Bool("k8", testBool), + 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 []attribute.KeyValue - r.WalkAttributes(func(kv attribute.KeyValue) bool { + var got []KeyValue + r.WalkAttributes(func(kv KeyValue) bool { got = append(got, kv) return true }) @@ -100,7 +104,7 @@ func TestRecordAttributes(t *testing.T) { } for _, tc := range testCases { i := 0 - r.WalkAttributes(func(kv attribute.KeyValue) bool { + r.WalkAttributes(func(kv KeyValue) bool { i++ return i < tc.index }) @@ -110,14 +114,14 @@ func TestRecordAttributes(t *testing.T) { func TestRecordAttributesInvalid(t *testing.T) { r := Record{} - attrs := []attribute.KeyValue{ - attribute.String("k1", testString), + attrs := []KeyValue{ + String("k1", testString), {}, - attribute.Int("k3", testInt), - attribute.Bool("k4", testBool), - attribute.String("k5", testString), - attribute.Float64("k6", testFloat), - attribute.Int("k7", testInt), + Int("k3", testInt), + Bool("k4", testBool), + String("k5", testString), + Float64("k6", testFloat), + Int("k7", testInt), {}, } r.AddAttributes(attrs...) @@ -138,11 +142,11 @@ func TestRecordAliasingAndClone(t *testing.T) { // creating a slice in r.back. r1 := Record{} for i := 0; i < attributesInlineCount+1; i++ { - r1.AddAttributes(attribute.Int("k", i)) + r1.AddAttributes(Int("k", i)) } // Ensure that r1.back's capacity exceeds its length. - b := make([]attribute.KeyValue, len(r1.back), len(r1.back)+1) + b := make([]KeyValue, len(r1.back), len(r1.back)+1) copy(b, r1.back) r1.back = b @@ -150,27 +154,27 @@ func TestRecordAliasingAndClone(t *testing.T) { // Adding to both should emit an special error for the second call. r2 := r1 r1AttrsBefore := attrsSlice(r1) - r1.AddAttributes(attribute.Int("p", 0)) + r1.AddAttributes(Int("p", 0)) assert.Zero(t, errs) - r2.AddAttributes(attribute.Int("p", 1)) + r2.AddAttributes(Int("p", 1)) assert.Equal(t, []error{errUnsafeAddAttrs}, errs, "sends an error via ErrorHandler when a dirty AddAttribute is detected") errs = nil - assert.Equal(t, append(r1AttrsBefore, attribute.Int("p", 0)), attrsSlice(r1)) - assert.Equal(t, append(r1AttrsBefore, attribute.Int("p", 1)), attrsSlice(r2)) + assert.Equal(t, append(r1AttrsBefore, Int("p", 0)), attrsSlice(r1)) + assert.Equal(t, append(r1AttrsBefore, Int("p", 1)), attrsSlice(r2)) // Adding to a clone is fine. r1Attrs := attrsSlice(r1) r3 := r1.Clone() assert.Equal(t, r1Attrs, attrsSlice(r3)) - r3.AddAttributes(attribute.Int("p", 2)) + r3.AddAttributes(Int("p", 2)) assert.Zero(t, errs) assert.Equal(t, r1Attrs, attrsSlice(r1), "r1 is unchanged") - assert.Equal(t, append(r1Attrs, attribute.Int("p", 2)), attrsSlice(r3)) + assert.Equal(t, append(r1Attrs, Int("p", 2)), attrsSlice(r3)) } -func attrsSlice(r Record) []attribute.KeyValue { - var attrs []attribute.KeyValue - r.WalkAttributes(func(kv attribute.KeyValue) bool { +func attrsSlice(r Record) []KeyValue { + var attrs []KeyValue + r.WalkAttributes(func(kv KeyValue) bool { attrs = append(attrs, kv) return true }) diff --git a/log/severity.go b/log/severity.go index c9076457be1..8b57e5e1441 100644 --- a/log/severity.go +++ b/log/severity.go @@ -1,10 +1,6 @@ // 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" // Severity represents a log record severity (also known as log level). diff --git a/log/slice.go b/log/slice.go new file mode 100644 index 00000000000..edb8f9001c5 --- /dev/null +++ b/log/slice.go @@ -0,0 +1,48 @@ +// 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 +} + +// sliceClip removes unused capacity from the slice, returning s[:len(s):len(s)]. +// +// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. +func sliceClip[S ~[]E, E any](s S) S { + return s[:len(s):len(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..80466429285 --- /dev/null +++ b/log/value.go @@ -0,0 +1,370 @@ +// 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 ( + "bytes" + "fmt" + "math" + "strconv" + "unsafe" +) + +// 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, listptr, 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 + listptr *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 + KindList + KindMap +) + +var kindStrings = []string{ + "Empty", + "Bool", + "Float64", + "Int64", + "String", + "Bytes", + "List", + "Map", +} + +var emptyString = []byte("") + +func (k Kind) String() string { + if k >= 0 && int(k) < len(kindStrings) { + return kindStrings[k] + } + return "" +} + +// 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 listptr: + return KindList + case mapptr: + return KindMap + default: + return KindEmpty + } +} + +// 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))} +} + +// ListValue returns a [Value] for a list of [Value]. +// The caller must not subsequently mutate the argument slice. +func ListValue(vs ...Value) Value { + return Value{num: uint64(len(vs)), any: listptr(unsafe.SliceData(vs))} +} + +// MapValue returns a new [Value] for a list 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))} +} + +// Any returns v's value as an any. +func (v Value) Any() any { + switch v.Kind() { + case KindMap: + return v.mapValue() + case KindList: + 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: + panic(fmt.Sprintf("bad kind: %s", v.Kind())) + } +} + +// String returns Value's value as a string, formatted like [fmt.Sprint]. Unlike +// the methods Int64, Float64, and so on, which panic if v is of the +// wrong kind, String never panics. +func (v Value) String() string { + if sp, ok := v.any.(stringptr); ok { + return unsafe.String(sp, v.num) + } + var buf []byte + return string(v.append(buf)) +} + +func (v Value) str() string { + return unsafe.String(v.any.(stringptr), v.num) +} + +// Int64 returns v's value as an int64. It panics +// if v is not a signed integer. +func (v Value) Int64() int64 { + if g, w := v.Kind(), KindInt64; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + return int64(v.num) +} + +// Bool returns v's value as a bool. It panics +// if v is not a bool. +func (v Value) Bool() bool { + if g, w := v.Kind(), KindBool; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + return v.bool() +} + +func (v Value) bool() bool { + return v.num == 1 +} + +// Float64 returns v's value as a float64. It panics +// if v is not a float64. +func (v Value) Float64() float64 { + if g, w := v.Kind(), KindFloat64; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + + return v.float() +} + +func (v Value) float() float64 { + return math.Float64frombits(v.num) +} + +// Map returns v's value as a []byte. +// It panics if v's [Kind] is not [KindBytes]. +func (v Value) Bytes() []byte { + if sp, ok := v.any.(bytesptr); ok { + return unsafe.Slice((*byte)(sp), v.num) + } + panic("Bytes: bad kind") +} + +func (v Value) bytes() []byte { + return unsafe.Slice((*byte)(v.any.(bytesptr)), v.num) +} + +// List returns v's value as a []Value. +// It panics if v's [Kind] is not [KindList]. +func (v Value) List() []Value { + if sp, ok := v.any.(listptr); ok { + return unsafe.Slice((*Value)(sp), v.num) + } + panic("List: bad kind") +} + +func (v Value) list() []Value { + return unsafe.Slice((*Value)(v.any.(listptr)), v.num) +} + +// Map returns v's value as a []KeyValue. +// It panics if v's [Kind] is not [KindMap]. +func (v Value) Map() []KeyValue { + if sp, ok := v.any.(mapptr); ok { + return unsafe.Slice((*KeyValue)(sp), v.num) + } + panic("Map: bad kind") +} + +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 KindList: + 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)) + } +} + +// append appends a text representation of v to dst. +// v is formatted as with fmt.Sprint. +func (v Value) append(dst []byte) []byte { + switch v.Kind() { + case KindString: + return append(dst, v.str()...) + case KindInt64: + return strconv.AppendInt(dst, int64(v.num), 10) + case KindFloat64: + return strconv.AppendFloat(dst, v.float(), 'g', -1, 64) + case KindBool: + return strconv.AppendBool(dst, v.bool()) + case KindBytes: + return fmt.Append(dst, v.bytes()) + case KindMap: + return fmt.Append(dst, v.mapValue()) + case KindList: + return fmt.Append(dst, v.list()) + case KindEmpty: + return append(dst, emptyString...) + default: + panic(fmt.Sprintf("bad kind: %s", v.Kind())) + } +} + +// An KeyValue is a key-value pair. +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)} +} + +// Bytes returns an KeyValue for a list of [Value]. +func List(key string, args ...Value) KeyValue { + return KeyValue{key, ListValue(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...)} +} + +// Invalid reports whether the key is empty. +func (a KeyValue) Invalid() bool { + return a.Key == "" +} + +// 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) +} + +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..d88dc860d76 --- /dev/null +++ b/log/value_test.go @@ -0,0 +1,191 @@ +// 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 TestKindString(t *testing.T) { + got := KindMap.String() + assert.Equal(t, "Map", got) +} + +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}), + ListValue(IntValue(3), StringValue("foo")), + MapValue(Bool("b", true), Int("i", 3)), + MapValue(List("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]"}, + {ListValue(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).Int64() + f = Float64Value(1).Float64() + b = BoolValue(true).Bool() + by = BytesValue(bytes).Bytes() + s = StringValue("foo").String() + })) + 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.Int64() + f = Float64("key", 1).Value.Float64() + b = Bool("key", true).Value.Bool() + by = Bytes("key", bytes).Value.Bytes() + s = String("key", "foo").Value.String() + })) + 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)}, ListValue(IntValue(3))}, + {[]KeyValue{Int("i", 3)}, MapValue(Int("i", 3))}, + {nil, Value{}}, + } { + got := test.in.Any() + assert.Equal(t, test.want, got) + } +} + +func TestEmptyMap(t *testing.T) { + g := Map("g") + got := g.Value.Map() + assert.Nil(t, got) +} + +func TestEmptyList(t *testing.T) { + l := ListValue() + got := l.List() + 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.Map() + 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 := ListValue(Value{}) + got := l.List() + 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.String() + } + } + _ = d +} From ecc26bc049a5f58f4c617e64a8aef8a40129f8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 26 Jan 2024 09:03:59 +0100 Subject: [PATCH 65/83] Record with pointer receivers only (#8) --- log/record.go | 18 +++++++++--------- log/record_test.go | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/log/record.go b/log/record.go index 0afb9600599..810a290a2bc 100644 --- a/log/record.go +++ b/log/record.go @@ -43,7 +43,7 @@ type Record struct { const attributesInlineCount = 5 // Timestamp returns the time when the log record occurred. -func (r Record) Timestamp() time.Time { +func (r *Record) Timestamp() time.Time { return r.timestamp } @@ -54,7 +54,7 @@ func (r *Record) SetTimestamp(t time.Time) { // 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 { +func (r *Record) ObservedTimestamp() time.Time { return r.observedTimestamp } @@ -65,7 +65,7 @@ func (r *Record) SetObservedTimestamp(t time.Time) { } // Severity returns the [Severity] of the log record. -func (r Record) Severity() Severity { +func (r *Record) Severity() Severity { return r.severity } @@ -78,7 +78,7 @@ func (r *Record) SetSeverity(s Severity) { // 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 { +func (r *Record) SeverityText() string { return r.severityText } @@ -90,7 +90,7 @@ func (r *Record) SetSeverityText(s string) { } // Body returns the the body of the log record as a strucutured value. -func (r Record) Body() Value { +func (r *Record) Body() Value { return r.body } @@ -101,7 +101,7 @@ func (r *Record) SetBody(v Value) { // WalkAttributes calls f on each [KeyValue] in the [Record]. // Iteration stops if f returns false. -func (r Record) WalkAttributes(f func(KeyValue) bool) { +func (r *Record) WalkAttributes(f func(KeyValue) bool) { for i := 0; i < r.nFront; i++ { if !f(r.front[i]) { return @@ -152,13 +152,13 @@ func (r *Record) AddAttributes(attrs ...KeyValue) { // 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 { +func (r *Record) Clone() Record { r.back = sliceClip(r.back) // prevent append from mutating shared array - return r + return *r } // AttributesLen returns the number of attributes in the Record. -func (r Record) AttributesLen() int { +func (r *Record) AttributesLen() int { return r.nFront + len(r.back) } diff --git a/log/record_test.go b/log/record_test.go index 0217c46baaf..078a73fd96d 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -170,6 +170,8 @@ func TestRecordAliasingAndClone(t *testing.T) { assert.Zero(t, errs) assert.Equal(t, r1Attrs, attrsSlice(r1), "r1 is unchanged") assert.Equal(t, append(r1Attrs, Int("p", 2)), attrsSlice(r3)) + r3.SetSeverity(SeverityDebug) + assert.NotEqual(t, r3.Severity(), r1.Severity(), "r1 is unchanged") } func attrsSlice(r Record) []KeyValue { From 2778c559f2d9f437db6d978e11b3f06f6f31d414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 26 Jan 2024 15:03:47 +0100 Subject: [PATCH 66/83] As[Type] Value methods --- log/internal/logr_test.go | 2 +- log/internal/slog_test.go | 2 +- log/internal/writer_logger.go | 12 ++++---- log/value.go | 55 ++++++++++++++++++++--------------- log/value_test.go | 32 ++++++++++---------- 5 files changed, 56 insertions(+), 47 deletions(-) diff --git a/log/internal/logr_test.go b/log/internal/logr_test.go index ba5620bf572..ff0aa7999cc 100644 --- a/log/internal/logr_test.go +++ b/log/internal/logr_test.go @@ -29,7 +29,7 @@ func TestLogrSink(t *testing.T) { 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.String()) + assert.Equal(t, testString, kv.Value.AsString()) return true }) } diff --git a/log/internal/slog_test.go b/log/internal/slog_test.go index 0b037dca6a4..19af0424476 100644 --- a/log/internal/slog_test.go +++ b/log/internal/slog_test.go @@ -28,7 +28,7 @@ func TestSlogHandler(t *testing.T) { 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.String()) + assert.Equal(t, testString, kv.Value.AsString()) return true }) } diff --git a/log/internal/writer_logger.go b/log/internal/writer_logger.go index fcbc87ccb73..a8d9f1f70a8 100644 --- a/log/internal/writer_logger.go +++ b/log/internal/writer_logger.go @@ -56,17 +56,17 @@ func (l *writerLogger) Emit(ctx context.Context, r log.Record) { func (l *writerLogger) appendValue(v log.Value) { switch v.Kind() { case log.KindString: - l.write(v.String()) + l.write(v.AsString()) case log.KindInt64: - l.write(strconv.FormatInt(v.Int64(), 10)) // strconv.FormatInt allocates memory. + l.write(strconv.FormatInt(v.AsInt64(), 10)) // strconv.FormatInt allocates memory. case log.KindFloat64: - l.write(strconv.FormatFloat(v.Float64(), 'g', -1, 64)) // strconv.FormatFloat allocates memory. + l.write(strconv.FormatFloat(v.AsFloat64(), 'g', -1, 64)) // strconv.FormatFloat allocates memory. case log.KindBool: - l.write(strconv.FormatBool(v.Bool())) + l.write(strconv.FormatBool(v.AsBool())) case log.KindBytes: - l.write(fmt.Sprint(v.Bytes())) + l.write(fmt.Sprint(v.AsBytes())) case log.KindMap: - l.write(fmt.Sprint(v.Map())) + l.write(fmt.Sprint(v.AsMap())) case log.KindEmpty: l.write("") default: diff --git a/log/value.go b/log/value.go index 80466429285..2fd911d720e 100644 --- a/log/value.go +++ b/log/value.go @@ -134,8 +134,8 @@ func MapValue(kvs ...KeyValue) Value { return Value{num: uint64(len(kvs)), any: mapptr(unsafe.SliceData(kvs))} } -// Any returns v's value as an any. -func (v Value) Any() any { +// AsAny returns v's value as an any. +func (v Value) AsAny() any { switch v.Kind() { case KindMap: return v.mapValue() @@ -158,33 +158,31 @@ func (v Value) Any() any { } } -// String returns Value's value as a string, formatted like [fmt.Sprint]. Unlike -// the methods Int64, Float64, and so on, which panic if v is of the -// wrong kind, String never panics. -func (v Value) String() string { +// AsString returns Value's value as a string, formatted like [fmt.Sprint]. It panics +// if v is not a string. +func (v Value) AsString() string { if sp, ok := v.any.(stringptr); ok { return unsafe.String(sp, v.num) } - var buf []byte - return string(v.append(buf)) + panic("AsString: bad kind") } func (v Value) str() string { return unsafe.String(v.any.(stringptr), v.num) } -// Int64 returns v's value as an int64. It panics +// AsInt64 returns v's value as an int64. It panics // if v is not a signed integer. -func (v Value) Int64() int64 { +func (v Value) AsInt64() int64 { if g, w := v.Kind(), KindInt64; g != w { panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) } return int64(v.num) } -// Bool returns v's value as a bool. It panics +// AsBool returns v's value as a bool. It panics // if v is not a bool. -func (v Value) Bool() bool { +func (v Value) AsBool() bool { if g, w := v.Kind(), KindBool; g != w { panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) } @@ -195,9 +193,9 @@ func (v Value) bool() bool { return v.num == 1 } -// Float64 returns v's value as a float64. It panics +// AsFloat64 returns v's value as a float64. It panics // if v is not a float64. -func (v Value) Float64() float64 { +func (v Value) AsFloat64() float64 { if g, w := v.Kind(), KindFloat64; g != w { panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) } @@ -209,39 +207,39 @@ func (v Value) float() float64 { return math.Float64frombits(v.num) } -// Map returns v's value as a []byte. +// AsBytes returns v's value as a []byte. // It panics if v's [Kind] is not [KindBytes]. -func (v Value) Bytes() []byte { +func (v Value) AsBytes() []byte { if sp, ok := v.any.(bytesptr); ok { return unsafe.Slice((*byte)(sp), v.num) } - panic("Bytes: bad kind") + panic("AsBytes: bad kind") } func (v Value) bytes() []byte { return unsafe.Slice((*byte)(v.any.(bytesptr)), v.num) } -// List returns v's value as a []Value. +// AsList returns v's value as a []Value. // It panics if v's [Kind] is not [KindList]. -func (v Value) List() []Value { +func (v Value) AsList() []Value { if sp, ok := v.any.(listptr); ok { return unsafe.Slice((*Value)(sp), v.num) } - panic("List: bad kind") + panic("AsList: bad kind") } func (v Value) list() []Value { return unsafe.Slice((*Value)(v.any.(listptr)), v.num) } -// Map returns v's value as a []KeyValue. +// AsMap returns v's value as a []KeyValue. // It panics if v's [Kind] is not [KindMap]. -func (v Value) Map() []KeyValue { +func (v Value) AsMap() []KeyValue { if sp, ok := v.any.(mapptr); ok { return unsafe.Slice((*KeyValue)(sp), v.num) } - panic("Map: bad kind") + panic("AsMap: bad kind") } func (v Value) mapValue() []KeyValue { @@ -280,6 +278,17 @@ func (v Value) Equal(w Value) bool { } } +// String returns Value's value as a string, formatted like [fmt.Sprint]. Unlike +// the methods Int64, Float64, and so on, which panic if v is of the +// wrong kind, String never panics. +func (v Value) String() string { + if sp, ok := v.any.(stringptr); ok { + return unsafe.String(sp, v.num) + } + var buf []byte + return string(v.append(buf)) +} + // append appends a text representation of v to dst. // v is formatted as with fmt.Sprint. func (v Value) append(dst []byte) []byte { diff --git a/log/value_test.go b/log/value_test.go index d88dc860d76..5b1108567e9 100644 --- a/log/value_test.go +++ b/log/value_test.go @@ -76,11 +76,11 @@ func TestValueNoAlloc(t *testing.T) { ) bytes := []byte{1, 3, 4} a := int(testing.AllocsPerRun(5, func() { - i = Int64Value(1).Int64() - f = Float64Value(1).Float64() - b = BoolValue(true).Bool() - by = BytesValue(bytes).Bytes() - s = StringValue("foo").String() + 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 @@ -101,11 +101,11 @@ func TestKeyValueNoAlloc(t *testing.T) { ) bytes := []byte{1, 3, 4} a := int(testing.AllocsPerRun(5, func() { - i = Int64("key", 1).Value.Int64() - f = Float64("key", 1).Value.Float64() - b = Bool("key", true).Value.Bool() - by = Bytes("key", bytes).Value.Bytes() - s = String("key", "foo").Value.String() + 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 @@ -130,20 +130,20 @@ func TestValueAny(t *testing.T) { {[]KeyValue{Int("i", 3)}, MapValue(Int("i", 3))}, {nil, Value{}}, } { - got := test.in.Any() + got := test.in.AsAny() assert.Equal(t, test.want, got) } } func TestEmptyMap(t *testing.T) { g := Map("g") - got := g.Value.Map() + got := g.Value.AsMap() assert.Nil(t, got) } func TestEmptyList(t *testing.T) { l := ListValue() - got := l.List() + got := l.AsList() assert.Nil(t, got) } @@ -153,7 +153,7 @@ func TestMapValueWithEmptyMaps(t *testing.T) { Int("a", 1), Map("g1", Map("g2")), Map("g3", Map("g4", Int("b", 2)))) - got := g.Map() + got := g.AsMap() want := []KeyValue{Int("a", 1), Map("g1", Map("g2")), Map("g3", Map("g4", Int("b", 2)))} assert.Equal(t, want, got) } @@ -161,7 +161,7 @@ func TestMapValueWithEmptyMaps(t *testing.T) { func TestListValueWithEmptyValues(t *testing.T) { // Preserve empty values. l := ListValue(Value{}) - got := l.List() + got := l.AsList() want := []Value{{}} assert.Equal(t, want, got) } @@ -184,7 +184,7 @@ func BenchmarkUnsafeStrings(b *testing.B) { for i := 0; i < b.N; i++ { copy(dst, src) for _, a := range dst { - d = a.String() + d = a.AsString() } } _ = d From 68d99a877be064852cd21bbc525d89563133da59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 26 Jan 2024 22:46:07 +0100 Subject: [PATCH 67/83] Use stringer for enum types --- log/kind_string.go | 30 +++++++++++++++++++++++++ log/severity.go | 50 ++++++++++++++++++++++-------------------- log/severity_string.go | 47 +++++++++++++++++++++++++++++++++++++++ log/value.go | 20 ++--------------- 4 files changed, 105 insertions(+), 42 deletions(-) create mode 100644 log/kind_string.go create mode 100644 log/severity_string.go diff --git a/log/kind_string.go b/log/kind_string.go new file mode 100644 index 00000000000..c398fb47200 --- /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[KindList-6] + _ = x[KindMap-7] +} + +const _Kind_name = "EmptyBoolFloat64Int64StringBytesListMap" + +var _Kind_index = [...]uint8{0, 5, 9, 16, 21, 27, 32, 36, 39} + +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/severity.go b/log/severity.go index 8b57e5e1441..c1d85740980 100644 --- a/log/severity.go +++ b/log/severity.go @@ -1,6 +1,8 @@ // 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). @@ -11,40 +13,40 @@ type Severity int // Severity values defined by OpenTelemetry. const ( // A fine-grained debugging log record. Typically disabled in default configurations. - SeverityTrace1 Severity = iota + 1 - SeverityTrace2 - SeverityTrace3 - SeverityTrace4 + SeverityTrace1 Severity = iota + 1 // TRACE + SeverityTrace2 // TRACE2 + SeverityTrace3 // TRACE3 + SeverityTrace4 // TRACE4 // A debugging log record. - SeverityDebug1 - SeverityDebug2 - SeverityDebug3 - SeverityDebug4 + SeverityDebug1 // DEBUG + SeverityDebug2 // DEBUG1 + SeverityDebug3 // DEBUG2 + SeverityDebug4 // DEBUG3 // An informational log record. Indicates that an event happened. - SeverityInfo1 - SeverityInfo2 - SeverityInfo3 - SeverityInfo4 + SeverityInfo1 // INFO + SeverityInfo2 // INFO1 + SeverityInfo3 // INFO2 + SeverityInfo4 // INFO3 // A warning log record. Not an error but is likely more important than an informational event. - SeverityWarn1 - SeverityWarn2 - SeverityWarn3 - SeverityWarn4 + SeverityWarn1 // WARN + SeverityWarn2 // WARN2 + SeverityWarn3 // WARN3 + SeverityWarn4 // WARN4 // An error log record. Something went wrong. - SeverityError1 - SeverityError2 - SeverityError3 - SeverityError4 + SeverityError1 // ERROR + SeverityError2 // ERROR2 + SeverityError3 // ERROR3 + SeverityError4 // ERROR4 // A fatal log record such as application or system crash. - SeverityFatal1 - SeverityFatal2 - SeverityFatal3 - SeverityFatal4 + SeverityFatal1 // FATAL + SeverityFatal2 // FATAL2 + SeverityFatal3 // FATAL3 + SeverityFatal4 // FATAL4 SeverityTrace = SeverityTrace1 SeverityDebug = SeverityDebug1 diff --git a/log/severity_string.go b/log/severity_string.go new file mode 100644 index 00000000000..5a534246df0 --- /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 = "TRACETRACE2TRACE3TRACE4DEBUGDEBUG1DEBUG2DEBUG3INFOINFO1INFO2INFO3WARNWARN2WARN3WARN4ERRORERROR2ERROR3ERROR4FATALFATAL2FATAL3FATAL4" + +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/value.go b/log/value.go index 2fd911d720e..d105b4f3075 100644 --- a/log/value.go +++ b/log/value.go @@ -5,6 +5,8 @@ // 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 ( @@ -49,26 +51,8 @@ const ( KindMap ) -var kindStrings = []string{ - "Empty", - "Bool", - "Float64", - "Int64", - "String", - "Bytes", - "List", - "Map", -} - var emptyString = []byte("") -func (k Kind) String() string { - if k >= 0 && int(k) < len(kindStrings) { - return kindStrings[k] - } - return "" -} - // Kind returns v's Kind. func (v Value) Kind() Kind { switch x := v.any.(type) { From e5dc7cf2f0e8b24fbcaf26cfd8b9f5fd5d4dc819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Fri, 26 Jan 2024 22:51:51 +0100 Subject: [PATCH 68/83] Revert Makefile changes --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2cedd200d3c..35fc189961b 100644 --- a/Makefile +++ b/Makefile @@ -315,4 +315,4 @@ add-tags: | $(MULTIMOD) .PHONY: lint-markdown lint-markdown: - docker run -v "$(CURDIR):$(WORKDIR)" avtodev/markdown-lint:v1 -c $(WORKDIR)/.markdownlint.yaml $(WORKDIR)/**/*.md + docker run -v "$(CURDIR):$(WORKDIR)" docker://avtodev/markdown-lint:v1 -c $(WORKDIR)/.markdownlint.yaml $(WORKDIR)/**/*.md From d4d42749c1b49ade837ceb5cb5de3f33f6441014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 30 Jan 2024 11:42:21 +0100 Subject: [PATCH 69/83] Fix Severity.String --- log/severity.go | 12 ++++++------ log/severity_string.go | 2 +- log/severity_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/log/severity.go b/log/severity.go index c1d85740980..3b8eef1569f 100644 --- a/log/severity.go +++ b/log/severity.go @@ -20,15 +20,15 @@ const ( // A debugging log record. SeverityDebug1 // DEBUG - SeverityDebug2 // DEBUG1 - SeverityDebug3 // DEBUG2 - SeverityDebug4 // DEBUG3 + SeverityDebug2 // DEBUG2 + SeverityDebug3 // DEBUG3 + SeverityDebug4 // DEBUG4 // An informational log record. Indicates that an event happened. SeverityInfo1 // INFO - SeverityInfo2 // INFO1 - SeverityInfo3 // INFO2 - SeverityInfo4 // INFO3 + SeverityInfo2 // INFO2 + SeverityInfo3 // INFO3 + SeverityInfo4 // INFO4 // A warning log record. Not an error but is likely more important than an informational event. SeverityWarn1 // WARN diff --git a/log/severity_string.go b/log/severity_string.go index 5a534246df0..d742ae5fe88 100644 --- a/log/severity_string.go +++ b/log/severity_string.go @@ -34,7 +34,7 @@ func _() { _ = x[SeverityFatal4-24] } -const _Severity_name = "TRACETRACE2TRACE3TRACE4DEBUGDEBUG1DEBUG2DEBUG3INFOINFO1INFO2INFO3WARNWARN2WARN3WARN4ERRORERROR2ERROR3ERROR4FATALFATAL2FATAL3FATAL4" +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} diff --git a/log/severity_test.go b/log/severity_test.go index 90dfde04f02..076befb7bca 100644 --- a/log/severity_test.go +++ b/log/severity_test.go @@ -16,161 +16,193 @@ func TestSeverity(t *testing.T) { 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()) }) } } From 91358b491aeedd720633b0d1b00c1ff1fd90432a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 30 Jan 2024 11:51:28 +0100 Subject: [PATCH 70/83] TestKind --- log/value_test.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/log/value_test.go b/log/value_test.go index 5b1108567e9..4642341fab1 100644 --- a/log/value_test.go +++ b/log/value_test.go @@ -15,9 +15,27 @@ import ( "github.com/stretchr/testify/assert" ) -func TestKindString(t *testing.T) { - got := KindMap.String() - assert.Equal(t, "Map", got) +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}, + {KindList, "List", 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) { From b90eca9c11974720d4f21d96f6d7df3ad12f0c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 30 Jan 2024 12:08:54 +0100 Subject: [PATCH 71/83] Refactor Value.String --- log/value.go | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/log/value.go b/log/value.go index d105b4f3075..183333b709e 100644 --- a/log/value.go +++ b/log/value.go @@ -51,7 +51,7 @@ const ( KindMap ) -var emptyString = []byte("") +var emptyString = "" // Kind returns v's Kind. func (v Value) Kind() Kind { @@ -266,33 +266,23 @@ func (v Value) Equal(w Value) bool { // the methods Int64, Float64, and so on, which panic if v is of the // wrong kind, String never panics. func (v Value) String() string { - if sp, ok := v.any.(stringptr); ok { - return unsafe.String(sp, v.num) - } - var buf []byte - return string(v.append(buf)) -} - -// append appends a text representation of v to dst. -// v is formatted as with fmt.Sprint. -func (v Value) append(dst []byte) []byte { switch v.Kind() { case KindString: - return append(dst, v.str()...) + return v.str() case KindInt64: - return strconv.AppendInt(dst, int64(v.num), 10) + return strconv.FormatInt(int64(v.num), 10) case KindFloat64: - return strconv.AppendFloat(dst, v.float(), 'g', -1, 64) + return strconv.FormatFloat(v.float(), 'g', -1, 64) case KindBool: - return strconv.AppendBool(dst, v.bool()) + return strconv.FormatBool(v.bool()) case KindBytes: - return fmt.Append(dst, v.bytes()) + return fmt.Sprint(v.bytes()) case KindMap: - return fmt.Append(dst, v.mapValue()) + return fmt.Sprint(v.mapValue()) case KindList: - return fmt.Append(dst, v.list()) + return fmt.Sprint(v.list()) case KindEmpty: - return append(dst, emptyString...) + return emptyString default: panic(fmt.Sprintf("bad kind: %s", v.Kind())) } @@ -358,6 +348,7 @@ 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) } From a02b8ea5d74832a5da8d6d621ee5c535162a6dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 08:49:34 +0100 Subject: [PATCH 72/83] Remove KeyValue.Invalid --- log/record.go | 36 ++++++------------------------------ log/record_test.go | 24 +++++------------------- log/value.go | 5 ----- 3 files changed, 11 insertions(+), 54 deletions(-) diff --git a/log/record.go b/log/record.go index 810a290a2bc..400cf03b2f4 100644 --- a/log/record.go +++ b/log/record.go @@ -123,30 +123,17 @@ func (r *Record) AddAttributes(attrs ...KeyValue) { var i int for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ { a := attrs[i] - if a.Invalid() { - continue - } r.front[r.nFront] = a r.nFront++ } - // Check if a copy was modified by slicing past the end - // and seeing if the attribute there is non-zero. + // Check if a copy was modified by slicing past the end. if cap(r.back) > len(r.back) { - end := r.back[:len(r.back)+1][len(r.back)] - if !end.Invalid() { - // Don't panic; copy and muddle through. - r.back = sliceClip(r.back) - otel.Handle(errUnsafeAddAttrs) - } - } - ne := countInvalidAttrs(attrs[i:]) - r.back = sliceGrow(r.back, len(attrs[i:])-ne) - for _, a := range attrs[i:] { - if a.Invalid() { - continue - } - r.back = append(r.back, a) + // Don't panic; copy and muddle through. + r.back = sliceClip(r.back) + otel.Handle(errUnsafeAddAttrs) } + r.back = sliceGrow(r.back, len(attrs[i:])) + r.back = append(r.back, attrs[i:]...) } // Clone returns a copy of the record with no shared state. @@ -161,14 +148,3 @@ func (r *Record) Clone() Record { func (r *Record) AttributesLen() int { return r.nFront + len(r.back) } - -// countInvalidAttrs returns the number of invalid attributes. -func countInvalidAttrs(as []KeyValue) int { - n := 0 - for _, a := range as { - if a.Invalid() { - n++ - } - } - return n -} diff --git a/log/record_test.go b/log/record_test.go index 078a73fd96d..d1bea31ebc3 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -77,6 +77,7 @@ func TestRecordAttributes(t *testing.T) { Float64("k6", testFloat), Int("k7", testInt), Bool("k8", testBool), + {}, } r.AddAttributes(attrs...) @@ -112,23 +113,6 @@ func TestRecordAttributes(t *testing.T) { } } -func TestRecordAttributesInvalid(t *testing.T) { - r := Record{} - attrs := []KeyValue{ - String("k1", testString), - {}, - Int("k3", testInt), - Bool("k4", testBool), - String("k5", testString), - Float64("k6", testFloat), - Int("k7", testInt), - {}, - } - r.AddAttributes(attrs...) - - assert.Equal(t, len(attrs)-2, r.AttributesLen()) -} - func TestRecordAliasingAndClone(t *testing.T) { defer func(orig otel.ErrorHandler) { otel.SetErrorHandler(orig) @@ -151,11 +135,12 @@ func TestRecordAliasingAndClone(t *testing.T) { r1.back = b // Make a copy that shares state. - // Adding to both should emit an special error for the second call. + // Adding to both should emit an special error for each call. r2 := r1 r1AttrsBefore := attrsSlice(r1) r1.AddAttributes(Int("p", 0)) - assert.Zero(t, errs) + assert.Equal(t, []error{errUnsafeAddAttrs}, errs, "sends an error via ErrorHandler when a dirty AddAttribute is detected") + errs = nil r2.AddAttributes(Int("p", 1)) assert.Equal(t, []error{errUnsafeAddAttrs}, errs, "sends an error via ErrorHandler when a dirty AddAttribute is detected") errs = nil @@ -168,6 +153,7 @@ func TestRecordAliasingAndClone(t *testing.T) { assert.Equal(t, r1Attrs, attrsSlice(r3)) r3.AddAttributes(Int("p", 2)) assert.Zero(t, errs) + errs = nil assert.Equal(t, r1Attrs, attrsSlice(r1), "r1 is unchanged") assert.Equal(t, append(r1Attrs, Int("p", 2)), attrsSlice(r3)) r3.SetSeverity(SeverityDebug) diff --git a/log/value.go b/log/value.go index 183333b709e..58961270b55 100644 --- a/log/value.go +++ b/log/value.go @@ -338,11 +338,6 @@ func Map(key string, args ...KeyValue) KeyValue { return KeyValue{key, MapValue(args...)} } -// Invalid reports whether the key is empty. -func (a KeyValue) Invalid() bool { - return a.Key == "" -} - // 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) From a8a1b7e53d768fea8c10b2e402025af689ce041f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 09:30:28 +0100 Subject: [PATCH 73/83] Update AddAttributes --- log/record.go | 13 +---------- log/record_test.go | 57 ++++++++++++++++++---------------------------- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/log/record.go b/log/record.go index 400cf03b2f4..ea59aeb5353 100644 --- a/log/record.go +++ b/log/record.go @@ -8,10 +8,7 @@ package log // import "go.opentelemetry.io/otel/log" import ( - "errors" "time" - - "go.opentelemetry.io/otel" ) // Record represents a log record. @@ -114,11 +111,8 @@ func (r *Record) WalkAttributes(f func(KeyValue) bool) { } } -var errUnsafeAddAttrs = errors.New("unsafely called AddAttributes on copy of Record made without using Record.Clone") - // AddAttributes appends the given [attribute.KeyValue] to the [Record]'s // list of [attribute.KeyValue]. -// It omits invalid attributes. func (r *Record) AddAttributes(attrs ...KeyValue) { var i int for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ { @@ -126,12 +120,7 @@ func (r *Record) AddAttributes(attrs ...KeyValue) { r.front[r.nFront] = a r.nFront++ } - // Check if a copy was modified by slicing past the end. - if cap(r.back) > len(r.back) { - // Don't panic; copy and muddle through. - r.back = sliceClip(r.back) - otel.Handle(errUnsafeAddAttrs) - } + r.back = sliceGrow(r.back, len(attrs[i:])) r.back = append(r.back, attrs[i:]...) } diff --git a/log/record_test.go b/log/record_test.go index d1bea31ebc3..9fbc7201489 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -8,12 +8,11 @@ package log import ( + "sync" "testing" "time" "github.com/stretchr/testify/assert" - - "go.opentelemetry.io/otel" ) var ( @@ -113,15 +112,7 @@ func TestRecordAttributes(t *testing.T) { } } -func TestRecordAliasingAndClone(t *testing.T) { - defer func(orig otel.ErrorHandler) { - otel.SetErrorHandler(orig) - }(otel.GetErrorHandler()) - var errs []error - otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { - errs = append(errs, err) - })) - +func TestRecordCloneConcurrentSafe(t *testing.T) { // Create a record whose Attrs overflow the inline array, // creating a slice in r.back. r1 := Record{} @@ -134,30 +125,26 @@ func TestRecordAliasingAndClone(t *testing.T) { copy(b, r1.back) r1.back = b - // Make a copy that shares state. - // Adding to both should emit an special error for each call. - r2 := r1 - r1AttrsBefore := attrsSlice(r1) - r1.AddAttributes(Int("p", 0)) - assert.Equal(t, []error{errUnsafeAddAttrs}, errs, "sends an error via ErrorHandler when a dirty AddAttribute is detected") - errs = nil - r2.AddAttributes(Int("p", 1)) - assert.Equal(t, []error{errUnsafeAddAttrs}, errs, "sends an error via ErrorHandler when a dirty AddAttribute is detected") - errs = nil - assert.Equal(t, append(r1AttrsBefore, Int("p", 0)), attrsSlice(r1)) - assert.Equal(t, append(r1AttrsBefore, Int("p", 1)), attrsSlice(r2)) - - // Adding to a clone is fine. - r1Attrs := attrsSlice(r1) - r3 := r1.Clone() - assert.Equal(t, r1Attrs, attrsSlice(r3)) - r3.AddAttributes(Int("p", 2)) - assert.Zero(t, errs) - errs = nil - assert.Equal(t, r1Attrs, attrsSlice(r1), "r1 is unchanged") - assert.Equal(t, append(r1Attrs, Int("p", 2)), attrsSlice(r3)) - r3.SetSeverity(SeverityDebug) - assert.NotEqual(t, r3.Severity(), r1.Severity(), "r1 is unchanged") + attrsBefore := attrsSlice(r1) + + // Changing this to r2 := r1 will make the test fail + // and has a race condition. + r2 := r1.Clone() + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + r1.AddAttributes(Int("p", 1)) + }() + go func() { + defer wg.Done() + r2.AddAttributes(Int("p", 2)) + }() + wg.Wait() + + assert.Equal(t, append(attrsBefore, Int("p", 1)), attrsSlice(r1)) + assert.Equal(t, append(attrsBefore, Int("p", 2)), attrsSlice(r2)) } func attrsSlice(r Record) []KeyValue { From c4a22095fd95a06740c73e15901ce47ca9292bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 09:30:56 +0100 Subject: [PATCH 74/83] Remove header --- log/record_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/log/record_test.go b/log/record_test.go index 9fbc7201489..baf8e32d24b 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -1,10 +1,6 @@ // 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 ( From 45cfdcb7b78b9102a7cfae6d67bcc1b51fe4bfb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 09:33:01 +0100 Subject: [PATCH 75/83] Update comment --- log/record_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/log/record_test.go b/log/record_test.go index baf8e32d24b..78aa347f2f9 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -123,8 +123,9 @@ func TestRecordCloneConcurrentSafe(t *testing.T) { attrsBefore := attrsSlice(r1) - // Changing this to r2 := r1 will make the test fail - // and has a race condition. + // Changing the line below to: + // r2 := r1 + // will cause assertion failures and a race condition. r2 := r1.Clone() var wg sync.WaitGroup From b55e22e16044d8f159ad44246b4e0d2821ee26ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 09:35:08 +0100 Subject: [PATCH 76/83] Refine comment --- log/record.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/log/record.go b/log/record.go index ea59aeb5353..aab8b7e66ad 100644 --- a/log/record.go +++ b/log/record.go @@ -129,7 +129,8 @@ func (r *Record) AddAttributes(attrs ...KeyValue) { // The original record and the clone can both be modified // without interfering with each other. func (r *Record) Clone() Record { - r.back = sliceClip(r.back) // prevent append from mutating shared array + // Prevent append from mutating shared array. + r.back = sliceClip(r.back) return *r } From 4dccab09940fedfdab9cadf2dfa9a69ce7d36a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 09:42:29 +0100 Subject: [PATCH 77/83] go mod tidy --- log/go.mod | 4 ---- log/go.sum | 5 ----- log/internal/go.mod | 2 -- log/internal/go.sum | 3 --- 4 files changed, 14 deletions(-) diff --git a/log/go.mod b/log/go.mod index 3072098ad4a..4def3819908 100644 --- a/log/go.mod +++ b/log/go.mod @@ -9,11 +9,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.23.0-rc.1 // indirect - go.opentelemetry.io/otel/trace v1.23.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/go.sum b/log/go.sum index 75d8b1f55b4..a6bcd03a15e 100644 --- a/log/go.sum +++ b/log/go.sum @@ -1,10 +1,5 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/log/internal/go.mod b/log/internal/go.mod index 5680a1f3aac..3b002eeff64 100644 --- a/log/internal/go.mod +++ b/log/internal/go.mod @@ -12,10 +12,8 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel v1.23.0-rc.1 // indirect - go.opentelemetry.io/otel/metric v1.23.0-rc.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/internal/go.sum b/log/internal/go.sum index 7274ecbf3a5..949da0b69c3 100644 --- a/log/internal/go.sum +++ b/log/internal/go.sum @@ -1,10 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= From a8f265cceb23bb32f665e91b340b1d87904100ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Thu, 1 Feb 2024 21:15:41 +0100 Subject: [PATCH 78/83] Refactor Severity to not use iota --- log/severity.go | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/log/severity.go b/log/severity.go index 3b8eef1569f..f411cfee4b4 100644 --- a/log/severity.go +++ b/log/severity.go @@ -13,40 +13,40 @@ type Severity int // Severity values defined by OpenTelemetry. const ( // A fine-grained debugging log record. Typically disabled in default configurations. - SeverityTrace1 Severity = iota + 1 // TRACE - SeverityTrace2 // TRACE2 - SeverityTrace3 // TRACE3 - SeverityTrace4 // TRACE4 + SeverityTrace1 Severity = 1 // TRACE + SeverityTrace2 Severity = 2 // TRACE2 + SeverityTrace3 Severity = 3 // TRACE3 + SeverityTrace4 Severity = 4 // TRACE4 // A debugging log record. - SeverityDebug1 // DEBUG - SeverityDebug2 // DEBUG2 - SeverityDebug3 // DEBUG3 - SeverityDebug4 // DEBUG4 + 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 // INFO - SeverityInfo2 // INFO2 - SeverityInfo3 // INFO3 - SeverityInfo4 // INFO4 + 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 // WARN - SeverityWarn2 // WARN2 - SeverityWarn3 // WARN3 - SeverityWarn4 // WARN4 + SeverityWarn1 Severity = 13 // WARN + SeverityWarn2 Severity = 14 // WARN2 + SeverityWarn3 Severity = 15 // WARN3 + SeverityWarn4 Severity = 16 // WARN4 // An error log record. Something went wrong. - SeverityError1 // ERROR - SeverityError2 // ERROR2 - SeverityError3 // ERROR3 - SeverityError4 // ERROR4 + 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 // FATAL - SeverityFatal2 // FATAL2 - SeverityFatal3 // FATAL3 - SeverityFatal4 // FATAL4 + SeverityFatal1 Severity = 21 // FATAL + SeverityFatal2 Severity = 22 // FATAL2 + SeverityFatal3 Severity = 23 // FATAL3 + SeverityFatal4 Severity = 24 // FATAL4 SeverityTrace = SeverityTrace1 SeverityDebug = SeverityDebug1 From b996e006f7ad42a3da3e861207416a935dcf8b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 7 Feb 2024 18:22:04 +0100 Subject: [PATCH 79/83] Remove Record.Clone as it is not necessary --- log/record.go | 9 --------- log/record_test.go | 37 ------------------------------------- log/slice.go | 7 ------- 3 files changed, 53 deletions(-) diff --git a/log/record.go b/log/record.go index aab8b7e66ad..5439c5ac9a7 100644 --- a/log/record.go +++ b/log/record.go @@ -125,15 +125,6 @@ func (r *Record) AddAttributes(attrs ...KeyValue) { r.back = append(r.back, attrs[i:]...) } -// 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 { - // Prevent append from mutating shared array. - r.back = sliceClip(r.back) - return *r -} - // 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 index 78aa347f2f9..73864e4fc60 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -4,7 +4,6 @@ package log import ( - "sync" "testing" "time" @@ -108,42 +107,6 @@ func TestRecordAttributes(t *testing.T) { } } -func TestRecordCloneConcurrentSafe(t *testing.T) { - // Create a record whose Attrs overflow the inline array, - // creating a slice in r.back. - r1 := Record{} - for i := 0; i < attributesInlineCount+1; i++ { - r1.AddAttributes(Int("k", i)) - } - - // Ensure that r1.back's capacity exceeds its length. - b := make([]KeyValue, len(r1.back), len(r1.back)+1) - copy(b, r1.back) - r1.back = b - - attrsBefore := attrsSlice(r1) - - // Changing the line below to: - // r2 := r1 - // will cause assertion failures and a race condition. - r2 := r1.Clone() - - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - r1.AddAttributes(Int("p", 1)) - }() - go func() { - defer wg.Done() - r2.AddAttributes(Int("p", 2)) - }() - wg.Wait() - - assert.Equal(t, append(attrsBefore, Int("p", 1)), attrsSlice(r1)) - assert.Equal(t, append(attrsBefore, Int("p", 2)), attrsSlice(r2)) -} - func attrsSlice(r Record) []KeyValue { var attrs []KeyValue r.WalkAttributes(func(kv KeyValue) bool { diff --git a/log/slice.go b/log/slice.go index edb8f9001c5..376a2c87936 100644 --- a/log/slice.go +++ b/log/slice.go @@ -20,13 +20,6 @@ func sliceGrow[S ~[]E, E any](s S, n int) S { return s } -// sliceClip removes unused capacity from the slice, returning s[:len(s):len(s)]. -// -// This is a copy from https://pkg.go.dev/slices as it is not available in Go 1.20. -func sliceClip[S ~[]E, E any](s S) S { - return s[:len(s):len(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 From ed67f0180d09b131a3f633373ada24082b749d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 7 Feb 2024 21:02:16 +0100 Subject: [PATCH 80/83] Refine KeyValue docs --- log/value.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/log/value.go b/log/value.go index 58961270b55..b931068bb06 100644 --- a/log/value.go +++ b/log/value.go @@ -289,6 +289,9 @@ func (v Value) String() string { } // 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 From 5851ead443dd3d78e658d6ecafb962ece061f6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 7 Feb 2024 21:07:19 +0100 Subject: [PATCH 81/83] Remove unused attrsSlice --- log/record_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/log/record_test.go b/log/record_test.go index 73864e4fc60..6f5300f7460 100644 --- a/log/record_test.go +++ b/log/record_test.go @@ -106,12 +106,3 @@ func TestRecordAttributes(t *testing.T) { assert.Equal(t, tc.index, i, "WalkAttributes early return for %s", tc.name) } } - -func attrsSlice(r Record) []KeyValue { - var attrs []KeyValue - r.WalkAttributes(func(kv KeyValue) bool { - attrs = append(attrs, kv) - return true - }) - return attrs -} From e89444ddacd833e8a267e063029f255a7e0c0419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Wed, 7 Feb 2024 21:14:08 +0100 Subject: [PATCH 82/83] go mod tidy --- log/internal/go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/log/internal/go.mod b/log/internal/go.mod index 3b002eeff64..44fd9f344dc 100644 --- a/log/internal/go.mod +++ b/log/internal/go.mod @@ -6,14 +6,14 @@ 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-rc.1 + 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-rc.1 // indirect + go.opentelemetry.io/otel v1.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From 147762ffb5c4394417b3f973274cc570723db79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Tue, 20 Feb 2024 15:19:12 +0100 Subject: [PATCH 83/83] value does not panic and change List to Slice --- log/kind_string.go | 6 +-- log/value.go | 96 ++++++++++++++++++++++++++-------------------- log/value_test.go | 18 ++++----- 3 files changed, 66 insertions(+), 54 deletions(-) diff --git a/log/kind_string.go b/log/kind_string.go index c398fb47200..bdfaa18665c 100644 --- a/log/kind_string.go +++ b/log/kind_string.go @@ -14,13 +14,13 @@ func _() { _ = x[KindInt64-3] _ = x[KindString-4] _ = x[KindBytes-5] - _ = x[KindList-6] + _ = x[KindSlice-6] _ = x[KindMap-7] } -const _Kind_name = "EmptyBoolFloat64Int64StringBytesListMap" +const _Kind_name = "EmptyBoolFloat64Int64StringBytesSliceMap" -var _Kind_index = [...]uint8{0, 5, 9, 16, 21, 27, 32, 36, 39} +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) { diff --git a/log/value.go b/log/value.go index b931068bb06..33012ae0be5 100644 --- a/log/value.go +++ b/log/value.go @@ -11,10 +11,13 @@ 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. @@ -25,14 +28,14 @@ type Value struct { // 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, listptr, sliceptr or mapptr) it contains the value. + // 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 - listptr *Value // used in Value.any when the Value is a []Value + sliceptr *Value // used in Value.any when the Value is a []Value mapptr *KeyValue // used in Value.any when the Value is a []KeyValue ) @@ -47,7 +50,7 @@ const ( KindInt64 KindString KindBytes - KindList + KindSlice KindMap ) @@ -62,8 +65,8 @@ func (v Value) Kind() Kind { return KindString case bytesptr: return KindBytes - case listptr: - return KindList + case sliceptr: + return KindSlice case mapptr: return KindMap default: @@ -71,6 +74,8 @@ func (v Value) Kind() Kind { } } +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))} @@ -106,24 +111,25 @@ func BytesValue(v []byte) Value { return Value{num: uint64(len(v)), any: bytesptr(unsafe.SliceData(v))} } -// ListValue returns a [Value] for a list of [Value]. +// SliceValue returns a [Value] for a slice of [Value]. // The caller must not subsequently mutate the argument slice. -func ListValue(vs ...Value) Value { - return Value{num: uint64(len(vs)), any: listptr(unsafe.SliceData(vs))} +func SliceValue(vs ...Value) Value { + return Value{num: uint64(len(vs)), any: sliceptr(unsafe.SliceData(vs))} } -// MapValue returns a new [Value] for a list of key-value pairs. +// 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 KindList: + case KindSlice: return v.list() case KindInt64: return int64(v.num) @@ -138,37 +144,41 @@ func (v Value) AsAny() any { case KindEmpty: return nil default: - panic(fmt.Sprintf("bad kind: %s", v.Kind())) + global.Error(errBadKind, "AsAny", "kind", v.Kind()) + return nil } } -// AsString returns Value's value as a string, formatted like [fmt.Sprint]. It panics -// if v is not a string. +// 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) } - panic("AsString: bad kind") + 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 panics -// if v is not a signed integer. +// 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 { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + global.Error(errBadKind, "AsInt64", "kind", v.Kind()) + return 0 } return int64(v.num) } -// AsBool returns v's value as a bool. It panics -// if v is not a bool. +// 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 { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + global.Error(errBadKind, "AsBool", "kind", v.Kind()) + return false } return v.bool() } @@ -177,11 +187,12 @@ func (v Value) bool() bool { return v.num == 1 } -// AsFloat64 returns v's value as a float64. It panics -// if v is not a float64. +// 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 { - panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + global.Error(errBadKind, "AsFloat64", "kind", v.Kind()) + return 0 } return v.float() @@ -192,38 +203,41 @@ func (v Value) float() float64 { } // AsBytes returns v's value as a []byte. -// It panics if v's [Kind] is not [KindBytes]. +// 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) } - panic("AsBytes: bad kind") + global.Error(errBadKind, "AsBytes", "kind", v.Kind()) + return nil } func (v Value) bytes() []byte { return unsafe.Slice((*byte)(v.any.(bytesptr)), v.num) } -// AsList returns v's value as a []Value. -// It panics if v's [Kind] is not [KindList]. -func (v Value) AsList() []Value { - if sp, ok := v.any.(listptr); ok { +// 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) } - panic("AsList: bad kind") + global.Error(errBadKind, "AsSlice", "kind", v.Kind()) + return nil } func (v Value) list() []Value { - return unsafe.Slice((*Value)(v.any.(listptr)), v.num) + return unsafe.Slice((*Value)(v.any.(sliceptr)), v.num) } // AsMap returns v's value as a []KeyValue. -// It panics if v's [Kind] is not [KindMap]. +// 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) } - panic("AsMap: bad kind") + global.Error(errBadKind, "AsMap", "kind", v.Kind()) + return nil } func (v Value) mapValue() []KeyValue { @@ -249,7 +263,7 @@ func (v Value) Equal(w Value) bool { return v.str() == w.str() case KindFloat64: return v.float() == w.float() - case KindList: + case KindSlice: return sliceEqualFunc(v.list(), w.list(), Value.Equal) case KindMap: return sliceEqualFunc(v.mapValue(), w.mapValue(), KeyValue.Equal) @@ -262,9 +276,7 @@ func (v Value) Equal(w Value) bool { } } -// String returns Value's value as a string, formatted like [fmt.Sprint]. Unlike -// the methods Int64, Float64, and so on, which panic if v is of the -// wrong kind, String never panics. +// String returns Value's value as a string, formatted like [fmt.Sprint]. func (v Value) String() string { switch v.Kind() { case KindString: @@ -279,12 +291,12 @@ func (v Value) String() string { return fmt.Sprint(v.bytes()) case KindMap: return fmt.Sprint(v.mapValue()) - case KindList: + case KindSlice: return fmt.Sprint(v.list()) case KindEmpty: return emptyString default: - panic(fmt.Sprintf("bad kind: %s", v.Kind())) + return "" } } @@ -328,9 +340,9 @@ func Bytes(key string, v []byte) KeyValue { return KeyValue{key, BytesValue(v)} } -// Bytes returns an KeyValue for a list of [Value]. -func List(key string, args ...Value) KeyValue { - return KeyValue{key, ListValue(args...)} +// 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]. diff --git a/log/value_test.go b/log/value_test.go index 4642341fab1..e936bd80f4e 100644 --- a/log/value_test.go +++ b/log/value_test.go @@ -26,7 +26,7 @@ func TestKind(t *testing.T) { {KindEmpty, "Empty", 0}, {KindFloat64, "Float64", 2}, {KindInt64, "Int64", 3}, - {KindList, "List", 6}, + {KindSlice, "Slice", 6}, {KindMap, "Map", 7}, {KindString, "String", 4}, } @@ -49,9 +49,9 @@ func TestValueEqual(t *testing.T) { BoolValue(false), StringValue("hi"), BytesValue([]byte{1, 3, 5}), - ListValue(IntValue(3), StringValue("foo")), + SliceValue(IntValue(3), StringValue("foo")), MapValue(Bool("b", true), Int("i", 3)), - MapValue(List("l", IntValue(3), StringValue("foo")), Bytes("b", []byte{3, 5, 7})), + MapValue(Slice("l", IntValue(3), StringValue("foo")), Bytes("b", []byte{3, 5, 7})), } for i, v1 := range vals { for j, v2 := range vals { @@ -74,7 +74,7 @@ func TestValueString(t *testing.T) { {BoolValue(true), "true"}, {StringValue("foo"), "foo"}, {BytesValue([]byte{2, 4, 6}), "[2 4 6]"}, - {ListValue(IntValue(3), StringValue("foo")), "[3 foo]"}, + {SliceValue(IntValue(3), StringValue("foo")), "[3 foo]"}, {MapValue(Int("a", 1), Bool("b", true)), "[a=1 b=true]"}, {Value{}, ""}, } { @@ -144,7 +144,7 @@ func TestValueAny(t *testing.T) { {int64(11), Int64Value(11)}, {1.5, Float64Value(1.5)}, {[]byte{1, 2, 3}, BytesValue([]byte{1, 2, 3})}, - {[]Value{IntValue(3)}, ListValue(IntValue(3))}, + {[]Value{IntValue(3)}, SliceValue(IntValue(3))}, {[]KeyValue{Int("i", 3)}, MapValue(Int("i", 3))}, {nil, Value{}}, } { @@ -160,8 +160,8 @@ func TestEmptyMap(t *testing.T) { } func TestEmptyList(t *testing.T) { - l := ListValue() - got := l.AsList() + l := SliceValue() + got := l.AsSlice() assert.Nil(t, got) } @@ -178,8 +178,8 @@ func TestMapValueWithEmptyMaps(t *testing.T) { func TestListValueWithEmptyValues(t *testing.T) { // Preserve empty values. - l := ListValue(Value{}) - got := l.AsList() + l := SliceValue(Value{}) + got := l.AsSlice() want := []Value{{}} assert.Equal(t, want, got) }