You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-08-10 22:31:50 +02:00
sdk/log: self-observability: log created metric (#7121)
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7015 Adds experimental `otel.sdk.log.created` metric to logger. Since this is experimental, metric is behind the OTEL_GO_X_SELF_OBSERVABILITY feature gate. Metric defined in https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/otel/sdk-metrics.md. Given the feature is experimental, it always uses the global meterprovider when enabled.
This commit is contained in:
@@ -49,6 +49,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||||||
- Add experimental self-observability span metrics in `go.opentelemetry.io/otel/sdk/trace`.
|
- Add experimental self-observability span metrics in `go.opentelemetry.io/otel/sdk/trace`.
|
||||||
Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027)
|
Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027)
|
||||||
- Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772)
|
- Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772)
|
||||||
|
- Add experimental self-observability log metrics in `go.opentelemetry.io/otel/sdk/log`.
|
||||||
|
Check the `go.opentelemetry.io/otel/sdk/log/internal/x` package documentation for more information. (#7121)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@@ -30,6 +30,9 @@ should be used to describe the unique runtime environment instrumented code
|
|||||||
is being run on. That way when multiple instances of the code are collected
|
is being run on. That way when multiple instances of the code are collected
|
||||||
at a single endpoint their origin is decipherable.
|
at a single endpoint their origin is decipherable.
|
||||||
|
|
||||||
|
See [go.opentelemetry.io/otel/sdk/log/internal/x] for information about
|
||||||
|
the experimental features.
|
||||||
|
|
||||||
See [go.opentelemetry.io/otel/log] for more information about
|
See [go.opentelemetry.io/otel/log] for more information about
|
||||||
the OpenTelemetry Logs API.
|
the OpenTelemetry Logs API.
|
||||||
*/
|
*/
|
||||||
|
@@ -9,7 +9,9 @@ require (
|
|||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
go.opentelemetry.io/otel v1.37.0
|
go.opentelemetry.io/otel v1.37.0
|
||||||
go.opentelemetry.io/otel/log v0.13.0
|
go.opentelemetry.io/otel/log v0.13.0
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0
|
go.opentelemetry.io/otel/sdk v1.37.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||||
go.opentelemetry.io/otel/trace v1.37.0
|
go.opentelemetry.io/otel/trace v1.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +20,6 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
34
sdk/log/internal/x/README.md
Normal file
34
sdk/log/internal/x/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Experimental Features
|
||||||
|
|
||||||
|
The Logs SDK contains features that have not yet stabilized in the OpenTelemetry specification.
|
||||||
|
These features are added to the OpenTelemetry Go Logs SDK prior to stabilization in the specification so that users can start experimenting with them and provide feedback.
|
||||||
|
|
||||||
|
These feature may change in backwards incompatible ways as feedback is applied.
|
||||||
|
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [Self-Observability](#self-observability)
|
||||||
|
|
||||||
|
### Self-Observability
|
||||||
|
|
||||||
|
The Logs SDK provides a self-observability feature that allows you to monitor the SDK itself.
|
||||||
|
|
||||||
|
To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`.
|
||||||
|
|
||||||
|
When enabled, the SDK will create the following metrics using the global `MeterProvider`:
|
||||||
|
|
||||||
|
- `otel.sdk.log.created`
|
||||||
|
|
||||||
|
Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentation for more details on these metrics.
|
||||||
|
|
||||||
|
[Semantic conventions for OpenTelemetry SDK metrics]: https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/otel/sdk-metrics.md
|
||||||
|
|
||||||
|
## Compatibility and Stability
|
||||||
|
|
||||||
|
Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md).
|
||||||
|
These features may be removed or modified in successive version releases, including patch versions.
|
||||||
|
|
||||||
|
When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release.
|
||||||
|
There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version.
|
||||||
|
If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support.
|
63
sdk/log/internal/x/x.go
Normal file
63
sdk/log/internal/x/x.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package x documents experimental features for [go.opentelemetry.io/otel/sdk/log].
|
||||||
|
package x // import "go.opentelemetry.io/otel/sdk/log/internal/x"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelfObservability is an experimental feature flag that determines if SDK
|
||||||
|
// self-observability metrics are enabled.
|
||||||
|
//
|
||||||
|
// To enable this feature set the OTEL_GO_X_SELF_OBSERVABILITY environment variable
|
||||||
|
// to the case-insensitive string value of "true" (i.e. "True" and "TRUE"
|
||||||
|
// will also enable this).
|
||||||
|
var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) {
|
||||||
|
if strings.EqualFold(v, "true") {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Feature is an experimental feature control flag. It provides a uniform way
|
||||||
|
// to interact with these feature flags and parse their values.
|
||||||
|
type Feature[T any] struct {
|
||||||
|
key string
|
||||||
|
parse func(v string) (T, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] {
|
||||||
|
const envKeyRoot = "OTEL_GO_X_"
|
||||||
|
return Feature[T]{
|
||||||
|
key: envKeyRoot + suffix,
|
||||||
|
parse: parse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the environment variable key that needs to be set to enable the
|
||||||
|
// feature.
|
||||||
|
func (f Feature[T]) Key() string { return f.key }
|
||||||
|
|
||||||
|
// Lookup returns the user configured value for the feature and true if the
|
||||||
|
// user has enabled the feature. Otherwise, if the feature is not enabled, a
|
||||||
|
// zero-value and false are returned.
|
||||||
|
func (f Feature[T]) Lookup() (v T, ok bool) {
|
||||||
|
// https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value
|
||||||
|
//
|
||||||
|
// > The SDK MUST interpret an empty value of an environment variable the
|
||||||
|
// > same way as when the variable is unset.
|
||||||
|
vRaw := os.Getenv(f.key)
|
||||||
|
if vRaw == "" {
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
return f.parse(vRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled reports whether the feature is enabled.
|
||||||
|
func (f Feature[T]) Enabled() bool {
|
||||||
|
_, ok := f.Lookup()
|
||||||
|
return ok
|
||||||
|
}
|
59
sdk/log/internal/x/x_test.go
Normal file
59
sdk/log/internal/x/x_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package x
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelfObservability(t *testing.T) {
|
||||||
|
const key = "OTEL_GO_X_SELF_OBSERVABILITY"
|
||||||
|
require.Equal(t, key, SelfObservability.Key())
|
||||||
|
|
||||||
|
t.Run("100", run(setenv(key, "100"), assertDisabled(SelfObservability)))
|
||||||
|
t.Run("true", run(setenv(key, "true"), assertEnabled(SelfObservability, "true")))
|
||||||
|
t.Run("True", run(setenv(key, "True"), assertEnabled(SelfObservability, "True")))
|
||||||
|
t.Run("false", run(setenv(key, "false"), assertDisabled(SelfObservability)))
|
||||||
|
t.Run("empty", run(assertDisabled(SelfObservability)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(steps ...func(*testing.T)) func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
for _, step := range steps {
|
||||||
|
step(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setenv(k, v string) func(t *testing.T) { //nolint:unparam // This is a reusable test utility function.
|
||||||
|
return func(t *testing.T) { t.Setenv(k, v) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEnabled[T any](f Feature[T], want T) func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
assert.True(t, f.Enabled(), "not enabled")
|
||||||
|
|
||||||
|
v, ok := f.Lookup()
|
||||||
|
assert.True(t, ok, "Lookup state")
|
||||||
|
assert.Equal(t, want, v, "Lookup value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertDisabled[T any](f Feature[T]) func(*testing.T) {
|
||||||
|
var zero T
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.False(t, f.Enabled(), "enabled")
|
||||||
|
|
||||||
|
v, ok := f.Lookup()
|
||||||
|
assert.False(t, ok, "Lookup state")
|
||||||
|
assert.Equal(t, zero, v, "Lookup value")
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,12 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/log"
|
"go.opentelemetry.io/otel/log"
|
||||||
"go.opentelemetry.io/otel/log/embedded"
|
"go.opentelemetry.io/otel/log/embedded"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk"
|
||||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log/internal/x"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.36.0"
|
||||||
|
"go.opentelemetry.io/otel/semconv/v1.36.0/otelconv"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,13 +29,35 @@ type logger struct {
|
|||||||
|
|
||||||
provider *LoggerProvider
|
provider *LoggerProvider
|
||||||
instrumentationScope instrumentation.Scope
|
instrumentationScope instrumentation.Scope
|
||||||
|
|
||||||
|
selfObservabilityEnabled bool
|
||||||
|
logCreatedMetric otelconv.SDKLogCreated
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLogger(p *LoggerProvider, scope instrumentation.Scope) *logger {
|
func newLogger(p *LoggerProvider, scope instrumentation.Scope) *logger {
|
||||||
return &logger{
|
l := &logger{
|
||||||
provider: p,
|
provider: p,
|
||||||
instrumentationScope: scope,
|
instrumentationScope: scope,
|
||||||
}
|
}
|
||||||
|
l.initSelfObservability()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) initSelfObservability() {
|
||||||
|
if !x.SelfObservability.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.selfObservabilityEnabled = true
|
||||||
|
mp := otel.GetMeterProvider()
|
||||||
|
m := mp.Meter("go.opentelemetry.io/otel/sdk/log",
|
||||||
|
metric.WithInstrumentationVersion(sdk.Version()),
|
||||||
|
metric.WithSchemaURL(semconv.SchemaURL))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if l.logCreatedMetric, err = otelconv.NewSDKLogCreated(m); err != nil {
|
||||||
|
otel.Handle(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) Emit(ctx context.Context, r log.Record) {
|
func (l *logger) Emit(ctx context.Context, r log.Record) {
|
||||||
@@ -96,6 +123,9 @@ func (l *logger) newRecord(ctx context.Context, r log.Record) Record {
|
|||||||
attributeCountLimit: l.provider.attributeCountLimit,
|
attributeCountLimit: l.provider.attributeCountLimit,
|
||||||
allowDupKeys: l.provider.allowDupKeys,
|
allowDupKeys: l.provider.allowDupKeys,
|
||||||
}
|
}
|
||||||
|
if l.selfObservabilityEnabled {
|
||||||
|
l.logCreatedMetric.Add(ctx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
// This field SHOULD be set once the event is observed by OpenTelemetry.
|
// This field SHOULD be set once the event is observed by OpenTelemetry.
|
||||||
if newRecord.observedTimestamp.IsZero() {
|
if newRecord.observedTimestamp.IsZero() {
|
||||||
|
@@ -6,15 +6,24 @@ package log // import "go.opentelemetry.io/otel/sdk/log"
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/log"
|
"go.opentelemetry.io/otel/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk"
|
||||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.36.0"
|
||||||
|
"go.opentelemetry.io/otel/semconv/v1.36.0/otelconv"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -348,3 +357,68 @@ func TestLoggerEnabled(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerSelfObservability(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
selfObservabilityEnabled bool
|
||||||
|
records []log.Record
|
||||||
|
wantLogRecordCount int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Disabled",
|
||||||
|
selfObservabilityEnabled: false,
|
||||||
|
records: []log.Record{{}, {}},
|
||||||
|
wantLogRecordCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Enabled",
|
||||||
|
selfObservabilityEnabled: true,
|
||||||
|
records: []log.Record{{}, {}, {}, {}, {}},
|
||||||
|
wantLogRecordCount: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", strconv.FormatBool(tc.selfObservabilityEnabled))
|
||||||
|
prev := otel.GetMeterProvider()
|
||||||
|
defer otel.SetMeterProvider(prev)
|
||||||
|
r := metric.NewManualReader()
|
||||||
|
mp := metric.NewMeterProvider(metric.WithReader(r))
|
||||||
|
otel.SetMeterProvider(mp)
|
||||||
|
l := newLogger(NewLoggerProvider(), instrumentation.Scope{})
|
||||||
|
|
||||||
|
for _, record := range tc.records {
|
||||||
|
l.Emit(context.Background(), record)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotMetrics := new(metricdata.ResourceMetrics)
|
||||||
|
assert.NoError(t, r.Collect(context.Background(), gotMetrics))
|
||||||
|
if tc.wantLogRecordCount == 0 {
|
||||||
|
assert.Empty(t, gotMetrics.ScopeMetrics)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, gotMetrics.ScopeMetrics, 1)
|
||||||
|
sm := gotMetrics.ScopeMetrics[0]
|
||||||
|
assert.Equal(t, instrumentation.Scope{
|
||||||
|
Name: "go.opentelemetry.io/otel/sdk/log",
|
||||||
|
Version: sdk.Version(),
|
||||||
|
SchemaURL: semconv.SchemaURL,
|
||||||
|
}, sm.Scope)
|
||||||
|
|
||||||
|
wantMetric := metricdata.Metrics{
|
||||||
|
Name: otelconv.SDKLogCreated{}.Name(),
|
||||||
|
Description: otelconv.SDKLogCreated{}.Description(),
|
||||||
|
Unit: otelconv.SDKLogCreated{}.Unit(),
|
||||||
|
Data: metricdata.Sum[int64]{
|
||||||
|
DataPoints: []metricdata.DataPoint[int64]{{Value: tc.wantLogRecordCount}},
|
||||||
|
Temporality: metricdata.CumulativeTemporality,
|
||||||
|
IsMonotonic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
metricdatatest.AssertEqual(t, wantMetric, sm.Metrics[0], metricdatatest.IgnoreTimestamp())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user