You've already forked opentelemetry-go
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:
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user