1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-03 18:35:08 +02:00

Added the internal/observ package to stdoutlog (#7735)

a part of #7020 

```txt
goos: windows
goarch: amd64
pkg: go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/observ
cpu: Intel(R) Core(TM) i7-14700
                                          │  result.txt  │
                                          │    sec/op    │
InstrumentationExportLogs/NoError-28        47.68n ±  5%
InstrumentationExportLogs/PartialError-28   471.6n ±  2%
InstrumentationExportLogs/FullError-28      471.9n ± 10%
geomean                                     219.7n

                                          │  result.txt  │
                                          │     B/op     │
InstrumentationExportLogs/NoError-28        0.000 ± 0%
InstrumentationExportLogs/PartialError-28   305.0 ± 0%
InstrumentationExportLogs/FullError-28      305.0 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                          │  result.txt  │
                                          │  allocs/op   │
InstrumentationExportLogs/NoError-28        0.000 ± 0%
InstrumentationExportLogs/PartialError-28   4.000 ± 0%
InstrumentationExportLogs/FullError-28      4.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean
```

---------

Co-authored-by: Damien Mathieu <42@dmathieu.com>
This commit is contained in:
ian
2026-03-18 16:48:36 +08:00
committed by GitHub
parent 5576bc22e7
commit 768e930779
13 changed files with 848 additions and 1 deletions
+2 -1
View File
@@ -9,9 +9,11 @@ require (
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/log v0.18.0
go.opentelemetry.io/otel/metric v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/sdk/log v0.18.0
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0
go.opentelemetry.io/otel/sdk/metric v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
)
@@ -23,7 +25,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.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
@@ -0,0 +1,9 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package internal provides internal functionality for the stdoutlog
// package.
package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
//go:generate gotmpl --body=../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutlog\" }" --out=x/x.go
//go:generate gotmpl --body=../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
@@ -0,0 +1,267 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package observ provides observability metrics for OTLP log exporters.
// This is an experimental feature controlled by the x.Observability feature flag.
package observ // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/observ"
import (
"context"
"errors"
"fmt"
"sync"
"time"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/x"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
)
const (
// ScopeName is the unique name of the meter used for instrumentation.
ScopeName = "go.opentelemetry.io/otel/exporters/stdoutlog/internal/observ"
// ComponentType uniquely identifies the OpenTelemetry Exporter component
// being instrumented.
//
// The STDOUT log exporter is not a standardized OTel component type, so
// it uses the Go package prefixed type name to ensure uniqueness and
// identity.
ComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutlog.Exporter"
// Version is the current version of this instrumentation.
//
// This matches the version of the exporter.
Version = internal.Version
)
var (
addOptPool = &sync.Pool{
New: func() any {
const n = 1
s := make([]metric.AddOption, 0, n)
return &s
},
}
attrsPool = &sync.Pool{
New: func() any {
const n = 1 + // component.name
1 + // component.type
1 // error.type
s := make([]attribute.KeyValue, 0, n)
return &s
},
}
recordOptPool = &sync.Pool{
New: func() any {
const n = 1
s := make([]metric.RecordOption, 0, n)
return &s
},
}
)
func get[T any](pool *sync.Pool) *[]T {
return pool.Get().(*[]T)
}
func put[T any](pool *sync.Pool, value *[]T) {
*value = (*value)[:0]
pool.Put(value)
}
// Instrumentation is experimental instrumentation for the exporter.
type Instrumentation struct {
inflight metric.Int64UpDownCounter
exported metric.Int64Counter
duration metric.Float64Histogram
attrs []attribute.KeyValue
addOpt metric.AddOption
recOpt metric.RecordOption
}
// GetComponentName returns the constant name for the exporter with the
// provided id.
func GetComponentName(id int64) string {
return fmt.Sprintf("%s/%d", ComponentType, id)
}
func getAttrs(id int64) []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 0, 2)
attrs = append(attrs,
semconv.OTelComponentName(GetComponentName(id)),
semconv.OTelComponentNameKey.String(ComponentType))
return attrs
}
// NewInstrumentation returns instrumentation for stdlog exporter.
func NewInstrumentation(id int64) (*Instrumentation, error) {
if !x.Observability.Enabled() {
return nil, nil
}
inst := &Instrumentation{}
mp := otel.GetMeterProvider()
m := mp.Meter(
ScopeName,
metric.WithInstrumentationVersion(Version),
metric.WithSchemaURL(semconv.SchemaURL),
)
var err error
inflight, e := otelconv.NewSDKExporterLogInflight(m)
if e != nil {
e = fmt.Errorf("failed to create the inflight metric: %w", e)
err = errors.Join(err, e)
}
inst.inflight = inflight.Inst()
exported, e := otelconv.NewSDKExporterLogExported(m)
if e != nil {
e = fmt.Errorf("failed to create the exported metric: %w", e)
err = errors.Join(err, e)
}
inst.exported = exported.Inst()
duration, e := otelconv.NewSDKExporterOperationDuration(m)
if e != nil {
e = fmt.Errorf("failed to create the duration metric: %w", e)
err = errors.Join(err, e)
}
inst.duration = duration.Inst()
if err != nil {
return nil, err
}
inst.attrs = getAttrs(id)
inst.addOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...))
inst.recOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...))
return inst, nil
}
// ExportLogs instruments the ExportLogs method of the exporter. It returns
// an [ExportOp] that must have its [ExportOp.End] method called when the
// ExportLogs method returns.
func (i *Instrumentation) ExportLogs(ctx context.Context, count int64) ExportOp {
start := time.Now()
addOpt := get[metric.AddOption](addOptPool)
defer put(addOptPool, addOpt)
*addOpt = append(*addOpt, i.addOpt)
i.inflight.Add(ctx, count, *addOpt...)
return ExportOp{
count: count,
ctx: ctx,
inst: i,
start: start,
}
}
// ExportOp tracks the operation being observed by [Instrumentation.ExportLogs].
type ExportOp struct {
count int64
ctx context.Context
inst *Instrumentation
start time.Time
}
// End completes the observation of the operation being observed by a call to
// [Instrumentation.ExportLogs].
// Any error that is encountered is provided as err.
//
// If err is not nil, all logs will be recorded as failures unless error is of
// type [internal.PartialSuccess]. In the case of a PartialSuccess, the number
// of successfully exported logs will be determined by inspecting the
// RejectedItems field of the PartialSuccess.
func (e ExportOp) End(err error) {
addOpt := get[metric.AddOption](addOptPool)
defer put(addOptPool, addOpt)
*addOpt = append(*addOpt, e.inst.addOpt)
e.inst.inflight.Add(e.ctx, -e.count, *addOpt...)
success := successful(err, e.count)
e.inst.exported.Add(e.ctx, success, *addOpt...)
if err != nil {
// Add the error.type attribute to the attribute set.
attrs := get[attribute.KeyValue](attrsPool)
defer put(attrsPool, attrs)
*attrs = append(*attrs, e.inst.attrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
o := metric.WithAttributeSet(attribute.NewSet(*attrs...))
*addOpt = append((*addOpt)[:0], o)
e.inst.exported.Add(e.ctx, e.count-success, *addOpt...)
}
recordOpt := get[metric.RecordOption](recordOptPool)
defer put(recordOptPool, recordOpt)
*recordOpt = append(*recordOpt, e.inst.recordOption(err))
e.inst.duration.Record(e.ctx, time.Since(e.start).Seconds(), *recordOpt...)
}
func (i *Instrumentation) recordOption(err error) metric.RecordOption {
if err == nil {
return i.recOpt
}
attrs := get[attribute.KeyValue](attrsPool)
defer put(attrsPool, attrs)
*attrs = append(*attrs, i.attrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
return metric.WithAttributeSet(attribute.NewSet(*attrs...))
}
// successful returns the number of successfully exported logs out of the n
// that were exported based on the provided error.
//
// If err is nil, n is returned. All logs were successfully exported.
//
// If err is not nil and not an [internal.PartialSuccess] error, 0 is returned.
// It is assumed all logs failed to be exported.
//
// If err is an [internal.PartialSuccess] error, the number of successfully
// exported logs is computed by subtracting the RejectedItems field from n. If
// RejectedItems is negative, n is returned. If RejectedItems is greater than
// n, 0 is returned.
func successful(err error, n int64) int64 {
if err == nil {
return n // All logs successfully exported.
}
// Split rejected calculation so successful is inlineable.
return n - rejectedCount(n, err)
}
var errPool = sync.Pool{
New: func() any {
return new(internal.PartialSuccess)
},
}
// rejectedCount returns how many out of the n logs exported were rejected based on
// the provided non-nil err.
func rejectedCount(n int64, err error) int64 {
ps := errPool.Get().(*internal.PartialSuccess)
defer errPool.Put(ps)
// check for partial success
if errors.As(err, ps) {
return min(max(ps.RejectedItems, 0), n)
}
// all logs exported
return n
}
@@ -0,0 +1,270 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package observ
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
)
const (
ID = 0
)
type errMeterProvider struct {
metric.MeterProvider
err error
}
func (m *errMeterProvider) Meter(string, ...metric.MeterOption) metric.Meter {
return &errMeter{err: m.err}
}
type errMeter struct {
metric.Meter
err error
}
func (e *errMeter) Int64UpDownCounter(string, ...metric.Int64UpDownCounterOption) (metric.Int64UpDownCounter, error) {
return nil, e.err
}
func (e *errMeter) Int64Counter(string, ...metric.Int64CounterOption) (metric.Int64Counter, error) {
return nil, e.err
}
func (e *errMeter) Float64Histogram(string, ...metric.Float64HistogramOption) (metric.Float64Histogram, error) {
return nil, e.err
}
func TestNewInstrumentation(t *testing.T) {
t.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
t.Run("NoError", func(t *testing.T) {
inst, err := NewInstrumentation(ID)
require.NoError(t, err)
assert.NotNil(t, inst.inflight, "logInflightMetric should be created")
assert.NotNil(t, inst.exported, "logExportedMetric should be created")
assert.NotNil(t, inst.duration, "logExportedDurationMetric should be created")
})
t.Run("error", func(t *testing.T) {
orig := otel.GetMeterProvider()
t.Cleanup(func() {
otel.SetMeterProvider(orig)
})
otel.SetMeterProvider(&errMeterProvider{
err: assert.AnError,
})
_, err := NewInstrumentation(ID)
require.ErrorIs(t, err, assert.AnError, "new instrument errors")
assert.ErrorContains(t, err, "inflight metric")
assert.ErrorContains(t, err, "exported metric")
assert.ErrorContains(t, err, "duration metric")
})
}
func set(err error) attribute.Set {
attrs := []attribute.KeyValue{
semconv.OTelComponentName(GetComponentName(ID)),
semconv.OTelComponentNameKey.String(ComponentType),
}
if err != nil {
attrs = append(attrs, semconv.ErrorType(err))
}
return attribute.NewSet(attrs...)
}
func logInflight() metricdata.Metrics {
inflight := otelconv.SDKExporterLogInflight{}
return metricdata.Metrics{
Name: inflight.Name(),
Description: inflight.Description(),
Unit: inflight.Unit(),
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.DataPoint[int64]{
{Attributes: set(nil), Value: 0},
},
},
}
}
func logExported(success, total int64, err error) metricdata.Metrics {
dp := []metricdata.DataPoint[int64]{
{Attributes: set(nil), Value: success},
}
if err != nil {
dp = append(dp, metricdata.DataPoint[int64]{
Attributes: set(err),
Value: total - success,
})
}
exported := otelconv.SDKExporterLogExported{}
return metricdata.Metrics{
Name: exported.Name(),
Description: exported.Description(),
Unit: exported.Unit(),
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: dp,
},
}
}
func logExportedDuration(err error) metricdata.Metrics {
attrs := set(err)
duration := otelconv.SDKExporterOperationDuration{}
return metricdata.Metrics{
Name: duration.Name(),
Description: duration.Description(),
Unit: duration.Unit(),
Data: metricdata.Histogram[float64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.HistogramDataPoint[float64]{
{Attributes: attrs},
},
},
}
}
func setup(t *testing.T) (*Instrumentation, func() metricdata.ScopeMetrics) {
t.Helper()
t.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
original := otel.GetMeterProvider()
t.Cleanup(func() {
otel.SetMeterProvider(original)
})
reader := sdkmetric.NewManualReader()
mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
otel.SetMeterProvider(mp)
inst, err := NewInstrumentation(ID)
require.NoError(t, err)
require.NotNil(t, inst)
return inst, func() metricdata.ScopeMetrics {
var rm metricdata.ResourceMetrics
require.NoError(t, reader.Collect(t.Context(), &rm))
require.Len(t, rm.ScopeMetrics, 1)
return rm.ScopeMetrics[0]
}
}
var Scope = instrumentation.Scope{
Name: ScopeName,
Version: internal.Version,
SchemaURL: semconv.SchemaURL,
}
func assertMetrics(
t *testing.T,
got metricdata.ScopeMetrics,
logs int64,
success int64,
err error,
) {
t.Helper()
assert.Equal(t, Scope, got.Scope)
m := got.Metrics
require.Len(t, m, 3)
o := metricdatatest.IgnoreTimestamp()
want := logInflight()
metricdatatest.AssertEqual(t, want, m[0], o)
want = logExported(success, logs, err)
metricdatatest.AssertEqual(t, want, m[1], o)
want = logExportedDuration(err)
metricdatatest.AssertEqual(t, want, m[2], metricdatatest.IgnoreValue(), o)
}
func TestInstrumentationExportLogs(t *testing.T) {
inst, collect := setup(t)
const n = 10
inst.ExportLogs(t.Context(), n).End(nil)
assertMetrics(t, collect(), n, n, nil)
}
func TestInstrumentationExportLogPartialErrors(t *testing.T) {
inst, collect := setup(t)
const n = 10
const success = 5
err := internal.PartialSuccess{RejectedItems: n - success}
inst.ExportLogs(t.Context(), n).End(err)
assertMetrics(t, collect(), n, success, err)
}
func TestInstrumentationExportLogAllErrors(t *testing.T) {
inst, collect := setup(t)
const n = 10
const success = 0
inst.ExportLogs(t.Context(), n).End(assert.AnError)
assertMetrics(t, collect(), n, success, assert.AnError)
}
func TestInstrumentationExportLogsInvalidPartialErrored(t *testing.T) {
inst, collect := setup(t)
const n = 10
err := internal.PartialSuccess{RejectedItems: -5}
inst.ExportLogs(t.Context(), n).End(err)
success := int64(n)
assertMetrics(t, collect(), n, success, err)
err.RejectedItems = n + 5
inst.ExportLogs(t.Context(), n).End(err)
success += 0
assertMetrics(t, collect(), n+n, success, err)
}
func BenchmarkInstrumentationExportLogs(b *testing.B) {
setup := func(tb *testing.B) *Instrumentation {
tb.Helper()
tb.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
inst, err := NewInstrumentation(ID)
if err != nil {
tb.Fatalf("failed to create instrumentation: %v", err)
}
return inst
}
run := func(err error) func(*testing.B) {
return func(b *testing.B) {
inst := setup(b)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
inst.ExportLogs(b.Context(), 10).End(err)
}
}
}
b.Run("NoError", run(nil))
b.Run("PartialError", run(&internal.PartialSuccess{RejectedItems: 6}))
b.Run("FullError", run(assert.AnError))
}
@@ -0,0 +1,42 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
import "fmt"
// PartialSuccess represents the underlying error for all handling
// stdoutlog partial success messages. Use `errors.Is(err, PartialSuccess{})`
// to test whether an error passed to the stdoutlog error handler belongs to this category.
type PartialSuccess struct {
ErrorMessage string
RejectedItems int64
RejectedKind string
}
var _ error = PartialSuccess{}
// Error implements the error interface.
func (ps PartialSuccess) Error() string {
msg := ps.ErrorMessage
if msg == "" {
msg = "empty message"
}
return fmt.Sprintf("stdoutlog partial success: %s (%d %s failed)", msg, ps.RejectedItems, ps.RejectedKind)
}
// Is supports the errors.Is() interface.
func (PartialSuccess) Is(err error) bool {
_, ok := err.(PartialSuccess)
return ok
}
// LogPartialSuccessError returns an error describing a partial success
// response for the log signal.
func LogPartialSuccessError(itemsRejected int64, errorMessage string) error {
return PartialSuccess{
ErrorMessage: errorMessage,
RejectedItems: itemsRejected,
RejectedKind: "logs",
}
}
@@ -0,0 +1,34 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package internal
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func requireErrorString(t *testing.T, expect string, err error) {
t.Helper()
require.Error(t, err)
require.ErrorIs(t, err, PartialSuccess{})
const pfx = "stdoutlog partial success: "
msg := err.Error()
require.True(t, strings.HasPrefix(msg, pfx))
require.Equal(t, expect, msg[len(pfx):])
}
func TestPartialSuccessFormat(t *testing.T) {
requireErrorString(t, "empty message (0 logs failed)", LogPartialSuccessError(0, ""))
requireErrorString(t, "help help (0 logs failed)", LogPartialSuccessError(0, "help help"))
requireErrorString(
t,
"what happened (10 logs failed)",
LogPartialSuccessError(10, "what happened"),
)
requireErrorString(t, "what happened (15 logs failed)", LogPartialSuccessError(15, "what happened"))
}
@@ -0,0 +1,8 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
// Version is the current release version of the OpenTelemetry stdoutlog
// exporter in use.
const Version = "v0.18.0"
@@ -0,0 +1,36 @@
# Experimental Features
The `stdoutlog` exporter contains features that have not yet stabilized in the OpenTelemetry specification.
These features are added to the `stdoutlog` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback.
These features may change in backwards incompatible ways as feedback is applied.
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.
## Features
- [Observability](#observability)
### Observability
The `stdoutlog` exporter can be configured to provide observability about itself using OpenTelemetry metrics.
To opt-in, set the environment variable `OTEL_GO_X_OBSERVABILITY` to `true`.
When enabled, the exporter will create the following metrics using the global `MeterProvider`:
- `otel.sdk.exporter.log.inflight`
- `otel.sdk.exporter.log.exported`
- `otel.sdk.exporter.operation.duration`
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.
@@ -0,0 +1,23 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/stdout/stdoutlog].
package x // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/x"
import "strings"
// Observability is an experimental feature flag that determines if exporter
// observability metrics are enabled.
//
// To enable this feature set the OTEL_GO_X_OBSERVABILITY environment variable
// to the case-insensitive string value of "true" (i.e. "True" and "TRUE"
// will also enable this).
var Observability = newFeature(
[]string{"OBSERVABILITY"},
func(v string) (string, bool) {
if strings.EqualFold(v, "true") {
return v, true
}
return "", false
},
)
@@ -0,0 +1,21 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package x
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestObservability(t *testing.T) {
const key = "OTEL_GO_X_OBSERVABILITY"
require.Contains(t, Observability.Keys(), key)
t.Run("100", run(setenv(key, "100"), assertDisabled(Observability)))
t.Run("true", run(setenv(key, "true"), assertEnabled(Observability, "true")))
t.Run("True", run(setenv(key, "True"), assertEnabled(Observability, "True")))
t.Run("false", run(setenv(key, "false"), assertDisabled(Observability)))
t.Run("empty", run(assertDisabled(Observability)))
}
@@ -0,0 +1,58 @@
// Code generated by gotmpl. DO NOT MODIFY.
// source: internal/shared/x/x.go.tmpl
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/stdout/stdoutlog].
package x // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/x"
import (
"os"
)
// 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 {
keys []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_"
keys := make([]string, 0, len(suffix))
for _, s := range suffix {
keys = append(keys, envKeyRoot+s)
}
return Feature[T]{
keys: keys,
parse: parse,
}
}
// Keys returns the environment variable keys that can be set to enable the
// feature.
func (f Feature[T]) Keys() []string { return f.keys }
// 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.
for _, key := range f.keys {
vRaw := os.Getenv(key)
if vRaw != "" {
return f.parse(vRaw)
}
}
return v, ok
}
// Enabled reports whether the feature is enabled.
func (f Feature[T]) Enabled() bool {
_, ok := f.Lookup()
return ok
}
@@ -0,0 +1,75 @@
// Code generated by gotmpl. DO NOT MODIFY.
// source: internal/shared/x/x_test.go.tmpl
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package x
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
mockKey = "OTEL_GO_X_MOCK_FEATURE"
mockKey2 = "OTEL_GO_X_MOCK_FEATURE2"
)
var mockFeature = newFeature([]string{"MOCK_FEATURE", "MOCK_FEATURE2"}, func(v string) (string, bool) {
if strings.EqualFold(v, "true") {
return v, true
}
return "", false
})
func TestFeature(t *testing.T) {
require.Contains(t, mockFeature.Keys(), mockKey)
require.Contains(t, mockFeature.Keys(), mockKey2)
t.Run("100", run(setenv(mockKey, "100"), assertDisabled(mockFeature)))
t.Run("true", run(setenv(mockKey, "true"), assertEnabled(mockFeature, "true")))
t.Run("True", run(setenv(mockKey, "True"), assertEnabled(mockFeature, "True")))
t.Run("false", run(setenv(mockKey, "false"), assertDisabled(mockFeature)))
t.Run("empty", run(assertDisabled(mockFeature)))
}
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")
}
}
+3
View File
@@ -64,3 +64,6 @@ modules:
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp:
version-refs:
- ./internal/version.go
go.opentelemetry.io/otel/exporters/stdout/stdoutlog:
version-refs:
- ./internal/version.go