Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032)
- 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)
- 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

Expand Down
3 changes: 3 additions & 0 deletions sdk/log/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
the OpenTelemetry Logs API.
*/
Expand Down
3 changes: 2 additions & 1 deletion sdk/log/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ require (
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel v1.37.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/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
)

Expand All @@ -18,7 +20,6 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/pmezard/go-difflib v1.0.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
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
34 changes: 34 additions & 0 deletions sdk/log/internal/x/README.md
Original file line number Diff line number Diff line change
@@ -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 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 changes: 63 additions & 0 deletions sdk/log/internal/x/x.go
Original file line number Diff line number Diff line change
@@ -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 changes: 59 additions & 0 deletions sdk/log/internal/x/x_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
32 changes: 31 additions & 1 deletion sdk/log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/log"
"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/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"
)

Expand All @@ -24,13 +29,35 @@ type logger struct {

provider *LoggerProvider
instrumentationScope instrumentation.Scope

selfObservabilityEnabled bool
logCreatedMetric otelconv.SDKLogCreated
}

func newLogger(p *LoggerProvider, scope instrumentation.Scope) *logger {
return &logger{
l := &logger{
provider: p,
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) {
Expand Down Expand Up @@ -96,6 +123,9 @@ func (l *logger) newRecord(ctx context.Context, r log.Record) Record {
attributeCountLimit: l.provider.attributeCountLimit,
allowDupKeys: l.provider.allowDupKeys,
}
if l.selfObservabilityEnabled {
l.logCreatedMetric.Add(ctx, 1)
}

// This field SHOULD be set once the event is observed by OpenTelemetry.
if newRecord.observedTimestamp.IsZero() {
Expand Down
74 changes: 74 additions & 0 deletions sdk/log/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ package log // import "go.opentelemetry.io/otel/sdk/log"
import (
"context"
"errors"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/log"
"go.opentelemetry.io/otel/sdk"
"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"
semconv "go.opentelemetry.io/otel/semconv/v1.36.0"
"go.opentelemetry.io/otel/semconv/v1.36.0/otelconv"
"go.opentelemetry.io/otel/trace"
)

Expand Down Expand Up @@ -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())
})
}
}
Loading