You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	Added the internal/observ package to otlploghttp (#7484)
				
					
				
			- Part of https://github.com/open-telemetry/opentelemetry-go/issues/7018 - Generate x package from shared template ``` goos: darwin goarch: arm64 pkg: go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal/observ cpu: Apple M3 │ bmark.results │ │ sec/op │ InstrumentationExportLogs/NoError-8 98.71n ± 0% InstrumentationExportLogs/PartialError-8 1.145µ ± 2% InstrumentationExportLogs/FullError-8 1.164µ ± 1% geomean 508.5n │ bmark.results │ │ B/op │ InstrumentationExportLogs/NoError-8 0.000 ± 0% InstrumentationExportLogs/PartialError-8 769.0 ± 0% InstrumentationExportLogs/FullError-8 769.0 ± 0% geomean ¹ ¹ summaries must be >0 to compute geomean │ bmark.results │ │ allocs/op │ InstrumentationExportLogs/NoError-8 0.000 ± 0% InstrumentationExportLogs/PartialError-8 5.000 ± 0% InstrumentationExportLogs/FullError-8 5.000 ± 0% geomean ¹ ¹ summaries must be >0 to compute geomean ``` --------- Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
		| @@ -7,13 +7,16 @@ retract v0.12.0 | ||||
|  | ||||
| require ( | ||||
| 	github.com/cenkalti/backoff/v5 v5.0.3 | ||||
| 	github.com/go-logr/logr v1.4.3 | ||||
| 	github.com/google/go-cmp v0.7.0 | ||||
| 	github.com/stretchr/testify v1.11.1 | ||||
| 	go.opentelemetry.io/otel v1.38.0 | ||||
| 	go.opentelemetry.io/otel/log v0.14.0 | ||||
| 	go.opentelemetry.io/otel/metric v1.38.0 | ||||
| 	go.opentelemetry.io/otel/sdk v1.38.0 | ||||
| 	go.opentelemetry.io/otel/sdk/log v0.14.0 | ||||
| 	go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 | ||||
| 	go.opentelemetry.io/otel/sdk/metric v1.38.0 | ||||
| 	go.opentelemetry.io/otel/trace v1.38.0 | ||||
| 	go.opentelemetry.io/proto/otlp v1.8.0 | ||||
| 	google.golang.org/protobuf v1.36.10 | ||||
| @@ -21,13 +24,11 @@ require ( | ||||
|  | ||||
| require ( | ||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
| 	github.com/go-logr/logr v1.4.3 // indirect | ||||
| 	github.com/go-logr/stdr v1.2.2 // indirect | ||||
| 	github.com/google/uuid v1.6.0 // indirect | ||||
| 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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.38.0 // indirect | ||||
| 	golang.org/x/net v0.45.0 // indirect | ||||
| 	golang.org/x/sys v0.37.0 // indirect | ||||
| 	golang.org/x/text v0.30.0 // indirect | ||||
|   | ||||
| @@ -5,6 +5,9 @@ | ||||
| // package. | ||||
| package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal" | ||||
|  | ||||
| //go:generate  gotmpl --body=../../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp\" }"  --out=x/x.go | ||||
| //go:generate gotmpl --body=../../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go | ||||
|  | ||||
| //go:generate gotmpl --body=../../../../../internal/shared/otlp/retry/retry.go.tmpl "--data={}" --out=retry/retry.go | ||||
| //go:generate gotmpl --body=../../../../../internal/shared/otlp/retry/retry_test.go.tmpl "--data={}" --out=retry/retry_test.go | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| // Copyright The OpenTelemetry Authors | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
|  | ||||
| // Package observ provides experimental observability instrumentation for the | ||||
| // otlploghttp exporter. | ||||
| package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal/observ" | ||||
| @@ -0,0 +1,347 @@ | ||||
| // Copyright The OpenTelemetry Authors | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
|  | ||||
| package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal/observ" | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/netip" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel" | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal" | ||||
| 	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal/x" | ||||
| 	"go.opentelemetry.io/otel/internal/global" | ||||
| 	"go.opentelemetry.io/otel/metric" | ||||
| 	semconv "go.opentelemetry.io/otel/semconv/v1.37.0" | ||||
| 	"go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// ScopeName is the unique name of the meter used for instrumentation. | ||||
| 	ScopeName = "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal/observ" | ||||
|  | ||||
| 	// Version is the current version of this instrumentation | ||||
| 	// | ||||
| 	// This matches the version of the exporter. | ||||
| 	Version = internal.Version | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	attrsPool = &sync.Pool{ | ||||
| 		New: func() any { | ||||
| 			const n = 1 + // component.name | ||||
| 				1 + // component.type | ||||
| 				1 + // server.addr | ||||
| 				1 + // server.port | ||||
| 				1 + // error.port | ||||
| 				1 // http.response.status.code | ||||
| 			s := make([]attribute.KeyValue, 0, n) | ||||
| 			return &s | ||||
| 		}, | ||||
| 	} | ||||
| 	addOptPool = &sync.Pool{ | ||||
| 		New: func() any { | ||||
| 			const n = 1 // WithAttributeSet | ||||
| 			s := make([]metric.AddOption, 0, n) | ||||
| 			return &s | ||||
| 		}, | ||||
| 	} | ||||
| 	recordPool = &sync.Pool{ | ||||
| 		New: func() any { | ||||
| 			const n = 1 // WithAttributeSet | ||||
| 			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) | ||||
| } | ||||
|  | ||||
| // GetComponentName returns the constant name for the exporter with the | ||||
| // provided id. | ||||
| func GetComponentName(id int64) string { | ||||
| 	return fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpHTTPLogExporter, id) | ||||
| } | ||||
|  | ||||
| // Instrumentation is experimental instrumentation for the exporter. | ||||
| type Instrumentation struct { | ||||
| 	inflightMetric    metric.Int64UpDownCounter | ||||
| 	exportedMetric    metric.Int64Counter | ||||
| 	operationDuration metric.Float64Histogram | ||||
|  | ||||
| 	presetAttrs []attribute.KeyValue | ||||
| 	addOpt      metric.AddOption | ||||
| 	recordOpt   metric.RecordOption | ||||
| } | ||||
|  | ||||
| // NewInstrumentation returns instrumentation for otlplog http exporter. | ||||
| func NewInstrumentation(id int64, target string) (*Instrumentation, error) { | ||||
| 	if !x.Observability.Enabled() { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	inst := &Instrumentation{} | ||||
|  | ||||
| 	provider := otel.GetMeterProvider() | ||||
| 	m := provider.Meter( | ||||
| 		ScopeName, | ||||
| 		metric.WithSchemaURL(semconv.SchemaURL), | ||||
| 		metric.WithInstrumentationVersion(Version), | ||||
| 	) | ||||
|  | ||||
| 	var e, err error | ||||
| 	logInflight, e := otelconv.NewSDKExporterLogInflight(m) | ||||
| 	if e != nil { | ||||
| 		e = fmt.Errorf("failed to create the inflight metric %w", e) | ||||
| 		err = errors.Join(err, e) | ||||
| 	} | ||||
| 	inst.inflightMetric = logInflight.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.exportedMetric = exported.Inst() | ||||
|  | ||||
| 	operation, e := otelconv.NewSDKExporterOperationDuration(m) | ||||
| 	if e != nil { | ||||
| 		e = fmt.Errorf("failed to create the operation duration metric %w", e) | ||||
| 		err = errors.Join(err, e) | ||||
| 	} | ||||
| 	inst.operationDuration = operation.Inst() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	inst.presetAttrs = setPresetAttrs(GetComponentName(id), target) | ||||
|  | ||||
| 	inst.addOpt = metric.WithAttributeSet(attribute.NewSet(inst.presetAttrs...)) | ||||
| 	inst.recordOpt = metric.WithAttributeSet(attribute.NewSet(append( | ||||
| 		[]attribute.KeyValue{semconv.HTTPResponseStatusCode(http.StatusOK)}, | ||||
| 		inst.presetAttrs..., | ||||
| 	)...)) | ||||
|  | ||||
| 	return inst, nil | ||||
| } | ||||
|  | ||||
| func setPresetAttrs(name, target string) []attribute.KeyValue { | ||||
| 	addrAttrs := ServerAddrAttrs(target) | ||||
|  | ||||
| 	attrs := make([]attribute.KeyValue, 0, 2+len(addrAttrs)) | ||||
| 	attrs = append( | ||||
| 		attrs, | ||||
| 		semconv.OTelComponentName(name), | ||||
| 		semconv.OTelComponentTypeOtlpHTTPLogExporter, | ||||
| 	) | ||||
| 	attrs = append(attrs, addrAttrs...) | ||||
| 	return attrs | ||||
| } | ||||
|  | ||||
| // ServerAddrAttrs is a function that extracts server address and port attributes | ||||
| // from a target string. | ||||
| func ServerAddrAttrs(target string) []attribute.KeyValue { | ||||
| 	host, port, err := parseTarget(target) | ||||
| 	if err != nil || (host == "" && port < 0) { | ||||
| 		if err != nil { | ||||
| 			global.Debug("failed to parse target", "target", target, "error", err) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if port < 0 { | ||||
| 		return []attribute.KeyValue{semconv.ServerAddress(host)} | ||||
| 	} | ||||
|  | ||||
| 	if host == "" { | ||||
| 		return []attribute.KeyValue{ | ||||
| 			semconv.ServerPort(port), | ||||
| 		} | ||||
| 	} | ||||
| 	return []attribute.KeyValue{ | ||||
| 		semconv.ServerAddress(host), | ||||
| 		semconv.ServerPort(port), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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.inflightMetric.Add(ctx, count, *addOpt...) | ||||
|  | ||||
| 	return ExportOp{ | ||||
| 		ctx:   ctx, | ||||
| 		start: start, | ||||
| 		inst:  i, | ||||
| 		count: count, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ExportOp tracks the operationDuration being observed by [Instrumentation.ExportLogs]. | ||||
| type ExportOp struct { | ||||
| 	ctx   context.Context | ||||
| 	start time.Time | ||||
| 	inst  *Instrumentation | ||||
| 	count int64 | ||||
| } | ||||
|  | ||||
| // End completes the observation of the operationDuration 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, code int) { | ||||
| 	addOpt := get[metric.AddOption](addOptPool) | ||||
| 	defer put(addOptPool, addOpt) | ||||
| 	*addOpt = append(*addOpt, e.inst.addOpt) | ||||
|  | ||||
| 	e.inst.inflightMetric.Add(e.ctx, -e.count, *addOpt...) | ||||
| 	success := successful(e.count, err) | ||||
| 	e.inst.exportedMetric.Add(e.ctx, success, *addOpt...) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		attrs := get[attribute.KeyValue](attrsPool) | ||||
| 		defer put(attrsPool, attrs) | ||||
|  | ||||
| 		*attrs = append(*attrs, e.inst.presetAttrs...) | ||||
| 		*attrs = append(*attrs, semconv.ErrorType(err)) | ||||
|  | ||||
| 		a := metric.WithAttributeSet(attribute.NewSet(*attrs...)) | ||||
| 		e.inst.exportedMetric.Add(e.ctx, e.count-success, a) | ||||
| 	} | ||||
|  | ||||
| 	record := get[metric.RecordOption](recordPool) | ||||
| 	defer put(recordPool, record) | ||||
| 	*record = append(*record, e.recordOption(err, code)) | ||||
|  | ||||
| 	duration := time.Since(e.start).Seconds() | ||||
| 	e.inst.operationDuration.Record(e.ctx, duration, *record...) | ||||
| } | ||||
|  | ||||
| func (e ExportOp) recordOption(err error, code int) metric.RecordOption { | ||||
| 	if err == nil { | ||||
| 		return e.inst.recordOpt | ||||
| 	} | ||||
|  | ||||
| 	attrs := get[attribute.KeyValue](attrsPool) | ||||
| 	defer put(attrsPool, attrs) | ||||
|  | ||||
| 	*attrs = append(*attrs, e.inst.presetAttrs...) | ||||
| 	*attrs = append( | ||||
| 		*attrs, | ||||
| 		semconv.HTTPResponseStatusCode(code), | ||||
| 		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(count int64, err error) int64 { | ||||
| 	if err == nil { | ||||
| 		return count | ||||
| 	} | ||||
| 	return count - rejected(count, err) | ||||
| } | ||||
|  | ||||
| var errPool = sync.Pool{ | ||||
| 	New: func() any { | ||||
| 		return new(internal.PartialSuccess) | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| // rejected returns how many out of the n logs exporter were rejected based on | ||||
| // the provided non-nil err. | ||||
| func rejected(n int64, err error) int64 { | ||||
| 	ps := errPool.Get().(*internal.PartialSuccess) | ||||
| 	defer errPool.Put(ps) | ||||
|  | ||||
| 	if errors.As(err, ps) { | ||||
| 		// Bound RejectedItems to [0, n]. This should not be needed, | ||||
| 		// but be defensive as this is from an external source. | ||||
| 		return min(max(ps.RejectedItems, 0), n) | ||||
| 	} | ||||
| 	// all logs exported | ||||
| 	return n | ||||
| } | ||||
|  | ||||
| // parseEndpoint parses the host and port from target that has the form | ||||
| // "host[:port]", or it returns an error if the target is not parsable. | ||||
| // | ||||
| // If no port is specified, -1 is returned. | ||||
| // | ||||
| // If no host is specified, an empty string is returned. | ||||
| func parseTarget(endpoint string) (string, int, error) { | ||||
| 	if ip := parseIP(endpoint); ip != "" { | ||||
| 		return ip, -1, nil | ||||
| 	} | ||||
|  | ||||
| 	// If there's no colon, there is no port (IPv6 with no port checked above). | ||||
| 	if !strings.Contains(endpoint, ":") { | ||||
| 		return endpoint, -1, nil | ||||
| 	} | ||||
|  | ||||
| 	// Otherwise, parse as host:port. | ||||
| 	host, portStr, err := net.SplitHostPort(endpoint) | ||||
| 	if err != nil { | ||||
| 		return "", -1, fmt.Errorf("invalid host:port %q: %w", endpoint, err) | ||||
| 	} | ||||
|  | ||||
| 	const base, bitSize = 10, 16 | ||||
| 	port16, err := strconv.ParseUint(portStr, base, bitSize) | ||||
| 	if err != nil { | ||||
| 		return "", -1, fmt.Errorf("invalid port %q: %w", portStr, err) | ||||
| 	} | ||||
| 	port := int(port16) | ||||
|  | ||||
| 	return host, port, nil | ||||
| } | ||||
|  | ||||
| // parseIP attempts to parse the entire target as an IP address. | ||||
| // It returns the normalized string form of the IP if successful, | ||||
| // or an empty string if parsing fails. | ||||
| func parseIP(ip string) string { | ||||
| 	// Strip leading and trailing brackets for IPv6 addresses. | ||||
| 	if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' { | ||||
| 		ip = ip[1 : len(ip)-1] | ||||
| 	} | ||||
| 	addr, err := netip.ParseAddr(ip) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	// Return the normalized string form of the IP. | ||||
| 	return addr.String() | ||||
| } | ||||
| @@ -0,0 +1,388 @@ | ||||
| // Copyright The OpenTelemetry Authors | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
|  | ||||
| package observ | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/go-logr/logr" | ||||
| 	"github.com/go-logr/logr/testr" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel" | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal" | ||||
| 	"go.opentelemetry.io/otel/internal/global" | ||||
| 	mapi "go.opentelemetry.io/otel/metric" | ||||
| 	"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" | ||||
| 	semconv "go.opentelemetry.io/otel/semconv/v1.37.0" | ||||
| 	"go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	ID     = 0 | ||||
| 	TARGET = "localhost:8080" | ||||
| ) | ||||
|  | ||||
| type errMeterProvider struct { | ||||
| 	mapi.MeterProvider | ||||
| 	err error | ||||
| } | ||||
|  | ||||
| func (m *errMeterProvider) Meter(string, ...mapi.MeterOption) mapi.Meter { | ||||
| 	return &errMeter{err: m.err} | ||||
| } | ||||
|  | ||||
| type errMeter struct { | ||||
| 	mapi.Meter | ||||
| 	err error | ||||
| } | ||||
|  | ||||
| func (m *errMeter) Int64UpDownCounter(string, ...mapi.Int64UpDownCounterOption) (mapi.Int64UpDownCounter, error) { | ||||
| 	return nil, m.err | ||||
| } | ||||
|  | ||||
| func (m *errMeter) Int64Counter(string, ...mapi.Int64CounterOption) (mapi.Int64Counter, error) { | ||||
| 	return nil, m.err | ||||
| } | ||||
|  | ||||
| func (m *errMeter) Float64Histogram(string, ...mapi.Float64HistogramOption) (mapi.Float64Histogram, error) { | ||||
| 	return nil, m.err | ||||
| } | ||||
|  | ||||
| func TestNewInstrumentationObservabilityErrors(t *testing.T) { | ||||
| 	orig := otel.GetMeterProvider() | ||||
| 	t.Cleanup(func() { otel.SetMeterProvider(orig) }) | ||||
| 	mp := &errMeterProvider{err: assert.AnError} | ||||
| 	otel.SetMeterProvider(mp) | ||||
|  | ||||
| 	t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") | ||||
|  | ||||
| 	_, err := NewInstrumentation(ID, TARGET) | ||||
| 	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, "operation duration metric") | ||||
| } | ||||
|  | ||||
| func TestNewInstrumentationObservabilityDisabled(t *testing.T) { | ||||
| 	// Do not set OTEL_GO_X_OBSERVABILITY. | ||||
| 	got, err := NewInstrumentation(ID, TARGET) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Nil(t, got) | ||||
| } | ||||
|  | ||||
| func set(err error) attribute.Set { | ||||
| 	attrs := []attribute.KeyValue{ | ||||
| 		semconv.OTelComponentName(GetComponentName(ID)), | ||||
| 		semconv.OTelComponentTypeKey.String(string(otelconv.ComponentTypeOtlpHTTPLogExporter)), | ||||
| 	} | ||||
| 	attrs = append(attrs, ServerAddrAttrs(TARGET)...) | ||||
| 	if err != nil { | ||||
| 		attrs = append(attrs, semconv.ErrorType(err)) | ||||
| 	} | ||||
| 	return attribute.NewSet(attrs...) | ||||
| } | ||||
|  | ||||
| func inflightMetric() 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 exportedMetric(err error, total, success int64) 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 operationDurationMetric(err error, code int) metricdata.Metrics { | ||||
| 	attrs := []attribute.KeyValue{ | ||||
| 		semconv.OTelComponentName(GetComponentName(ID)), | ||||
| 		semconv.OTelComponentTypeOtlpHTTPLogExporter, | ||||
| 		semconv.HTTPResponseStatusCode(code), | ||||
| 	} | ||||
| 	attrs = append(attrs, ServerAddrAttrs(TARGET)...) | ||||
| 	if err != nil { | ||||
| 		attrs = append(attrs, semconv.ErrorType(err)) | ||||
| 	} | ||||
|  | ||||
| 	operation := otelconv.SDKExporterOperationDuration{} | ||||
|  | ||||
| 	return metricdata.Metrics{ | ||||
| 		Name:        operation.Name(), | ||||
| 		Description: operation.Description(), | ||||
| 		Unit:        operation.Unit(), | ||||
| 		Data: metricdata.Histogram[float64]{ | ||||
| 			Temporality: metricdata.CumulativeTemporality, | ||||
| 			DataPoints: []metricdata.HistogramDataPoint[float64]{ | ||||
| 				{ | ||||
| 					Attributes: attribute.NewSet(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) | ||||
| 	}) | ||||
|  | ||||
| 	r := metric.NewManualReader() | ||||
| 	mp := metric.NewMeterProvider(metric.WithReader(r)) | ||||
| 	otel.SetMeterProvider(mp) | ||||
|  | ||||
| 	inst, err := NewInstrumentation(ID, TARGET) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, inst) | ||||
|  | ||||
| 	return inst, func() metricdata.ScopeMetrics { | ||||
| 		var rm metricdata.ResourceMetrics | ||||
| 		require.NoError(t, r.Collect(t.Context(), &rm)) | ||||
| 		require.Len(t, rm.ScopeMetrics, 1) | ||||
| 		return rm.ScopeMetrics[0] | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var Scope = instrumentation.Scope{ | ||||
| 	Name:      ScopeName, | ||||
| 	Version:   Version, | ||||
| 	SchemaURL: semconv.SchemaURL, | ||||
| } | ||||
|  | ||||
| func assertMetrics( | ||||
| 	t *testing.T, | ||||
| 	got metricdata.ScopeMetrics, | ||||
| 	count int64, | ||||
| 	success int64, | ||||
| 	err error, | ||||
| 	code int, | ||||
| ) { | ||||
| 	t.Helper() | ||||
| 	assert.Equal(t, Scope, got.Scope, "unexpected scope") | ||||
| 	m := got.Metrics | ||||
| 	require.Len(t, m, 3, "expected 3 metrics") | ||||
|  | ||||
| 	o := metricdatatest.IgnoreTimestamp() | ||||
| 	want := inflightMetric() | ||||
| 	metricdatatest.AssertEqual(t, want, m[0], o) | ||||
|  | ||||
| 	want = exportedMetric(err, count, success) | ||||
| 	metricdatatest.AssertEqual(t, want, m[1], o) | ||||
|  | ||||
| 	want = operationDurationMetric(err, code) | ||||
| 	metricdatatest.AssertEqual(t, want, m[2], metricdatatest.IgnoreValue(), o) | ||||
| } | ||||
|  | ||||
| func TestInstrumentationExportedLogs(t *testing.T) { | ||||
| 	inst, collect := setup(t) | ||||
| 	const n = 10 | ||||
| 	inst.ExportLogs(t.Context(), n).End(nil, http.StatusOK) | ||||
| 	assertMetrics(t, collect(), n, n, nil, http.StatusOK) | ||||
| } | ||||
|  | ||||
| func TestInstrumentationExportLogsPartialErrors(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, http.StatusPartialContent) | ||||
|  | ||||
| 	assertMetrics(t, collect(), n, success, err, http.StatusPartialContent) | ||||
| } | ||||
|  | ||||
| func TestInstrumentationExportLogAllErrors(t *testing.T) { | ||||
| 	inst, collect := setup(t) | ||||
| 	const n = 10 | ||||
| 	const success = 0 | ||||
|  | ||||
| 	inst.ExportLogs(t.Context(), n).End(assert.AnError, http.StatusUnauthorized) | ||||
|  | ||||
| 	assertMetrics(t, collect(), n, success, assert.AnError, http.StatusUnauthorized) | ||||
| } | ||||
|  | ||||
| func TestInstrumentationExportLogsInvalidPartialErrored(t *testing.T) { | ||||
| 	inst, collect := setup(t) | ||||
| 	const n = 10 | ||||
|  | ||||
| 	err := internal.PartialSuccess{RejectedItems: -5} | ||||
| 	inst.ExportLogs(t.Context(), n).End(err, http.StatusPartialContent) | ||||
|  | ||||
| 	success := n | ||||
| 	assertMetrics(t, collect(), n, int64(success), err, http.StatusPartialContent) | ||||
|  | ||||
| 	err.RejectedItems = n + 5 | ||||
| 	inst.ExportLogs(t.Context(), n).End(err, http.StatusPartialContent) | ||||
|  | ||||
| 	success += 0 | ||||
| 	assertMetrics(t, collect(), n+n, int64(success), err, http.StatusPartialContent) | ||||
| } | ||||
|  | ||||
| func TestSetPresetAttrs(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		endpoint string | ||||
| 		host     string | ||||
| 		port     int | ||||
| 	}{ | ||||
| 		// Empty. | ||||
| 		{endpoint: "", host: "", port: -1}, | ||||
|  | ||||
| 		// Only a port. | ||||
| 		{endpoint: ":4318", host: "", port: 4318}, | ||||
|  | ||||
| 		// Hostname. | ||||
| 		{endpoint: "localhost:4318", host: "localhost", port: 4318}, | ||||
| 		{endpoint: "localhost", host: "localhost", port: -1}, | ||||
|  | ||||
| 		// IPv4 address. | ||||
| 		{endpoint: "127.0.0.1:4318", host: "127.0.0.1", port: 4318}, | ||||
| 		{endpoint: "127.0.0.1", host: "127.0.0.1", port: -1}, | ||||
|  | ||||
| 		// IPv6 address. | ||||
| 		{endpoint: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", host: "2001:db8:85a3::8a2e:370:7334", port: -1}, | ||||
| 		{endpoint: "2001:db8:85a3:0:0:8a2e:370:7334", host: "2001:db8:85a3::8a2e:370:7334", port: -1}, | ||||
| 		{endpoint: "2001:db8:85a3::8a2e:370:7334", host: "2001:db8:85a3::8a2e:370:7334", port: -1}, | ||||
| 		{endpoint: "[2001:db8:85a3::8a2e:370:7334]", host: "2001:db8:85a3::8a2e:370:7334", port: -1}, | ||||
| 		{endpoint: "[::1]:9090", host: "::1", port: 9090}, | ||||
|  | ||||
| 		// Port edge cases. | ||||
| 		{endpoint: "example.com:0", host: "example.com", port: 0}, | ||||
| 		{endpoint: "example.com:65535", host: "example.com", port: 65535}, | ||||
|  | ||||
| 		// Case insensitive. | ||||
| 		{endpoint: "ExAmPlE.COM:8080", host: "ExAmPlE.COM", port: 8080}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		got := setPresetAttrs(GetComponentName(ID), tt.endpoint) | ||||
| 		want := []attribute.KeyValue{ | ||||
| 			semconv.OTelComponentName(GetComponentName(ID)), | ||||
| 			semconv.OTelComponentTypeOtlpHTTPLogExporter, | ||||
| 		} | ||||
|  | ||||
| 		if tt.host != "" { | ||||
| 			want = append(want, semconv.ServerAddress(tt.host)) | ||||
| 		} | ||||
| 		if tt.port != -1 { | ||||
| 			want = append(want, semconv.ServerPort(tt.port)) | ||||
| 		} | ||||
| 		assert.Equal(t, want, got) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type logSink struct { | ||||
| 	logr.LogSink | ||||
|  | ||||
| 	level         int | ||||
| 	msg           string | ||||
| 	keysAndValues []any | ||||
| } | ||||
|  | ||||
| func (*logSink) Enabled(int) bool { return true } | ||||
|  | ||||
| func (l *logSink) Info(level int, msg string, keysAndValues ...any) { | ||||
| 	l.level, l.msg, l.keysAndValues = level, msg, keysAndValues | ||||
| 	l.LogSink.Info(level, msg, keysAndValues...) | ||||
| } | ||||
|  | ||||
| func TestSetPresetAttrsError(t *testing.T) { | ||||
| 	endpoints := []string{ | ||||
| 		"example.com:invalid",   // Non-numeric port. | ||||
| 		"example.com:8080:9090", // Multiple colons in port. | ||||
| 		"example.com:99999",     // Port out of range. | ||||
| 		"example.com:-1",        // Port out of range. | ||||
| 	} | ||||
| 	for _, endpoint := range endpoints { | ||||
| 		l := &logSink{LogSink: testr.New(t).GetSink()} | ||||
| 		t.Cleanup(func(orig logr.Logger) func() { | ||||
| 			global.SetLogger(logr.New(l)) | ||||
| 			return func() { global.SetLogger(orig) } | ||||
| 		}(global.GetLogger())) | ||||
|  | ||||
| 		// Set the logger as global so BaseAttrs can log the error. | ||||
| 		got := setPresetAttrs(GetComponentName(ID), endpoint) | ||||
| 		want := []attribute.KeyValue{ | ||||
| 			semconv.OTelComponentName(GetComponentName(ID)), | ||||
| 			semconv.OTelComponentTypeOtlpHTTPLogExporter, | ||||
| 		} | ||||
| 		assert.Equal(t, want, got) | ||||
|  | ||||
| 		assert.Equal(t, 8, l.level, "expected Debug log level") | ||||
| 		assert.Equal(t, "failed to parse target", l.msg) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkInstrumentationExportLogs(b *testing.B) { | ||||
| 	setup := func(b *testing.B) *Instrumentation { | ||||
| 		b.Helper() | ||||
| 		b.Setenv("OTEL_GO_X_OBSERVABILITY", "true") | ||||
| 		inst, err := NewInstrumentation(ID, TARGET) | ||||
| 		if err != nil { | ||||
| 			b.Fatalf("failed to create instrumentation: %v", err) | ||||
| 		} | ||||
| 		return inst | ||||
| 	} | ||||
|  | ||||
| 	run := func(err error, statusCode int) 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, statusCode) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.Run("NoError", run(nil, http.StatusOK)) | ||||
| 	err := &internal.PartialSuccess{RejectedItems: 6} | ||||
| 	b.Run("PartialError", run(err, http.StatusOK)) | ||||
| 	b.Run("FullError", run(assert.AnError, http.StatusInternalServerError)) | ||||
| } | ||||
| @@ -0,0 +1,42 @@ | ||||
| // Copyright The OpenTelemetry Authors | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
|  | ||||
| package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal" | ||||
|  | ||||
| import "fmt" | ||||
|  | ||||
| // PartialSuccess represents the underlying error for all handling | ||||
| // OTLP partial success messages.  Use `errors.Is(err, | ||||
| // PartialSuccess{})` to test whether an error passed to the OTel | ||||
| // 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("OTLP partial success: %s (%d %s rejected)", 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. | ||||
| 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 = "OTLP 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 rejected)", LogPartialSuccessError(0, "")) | ||||
| 	requireErrorString(t, "help help (0 logs rejected)", LogPartialSuccessError(0, "help help")) | ||||
| 	requireErrorString( | ||||
| 		t, | ||||
| 		"what happened (10 logs rejected)", | ||||
| 		LogPartialSuccessError(10, "what happened"), | ||||
| 	) | ||||
| 	requireErrorString(t, "what happened (15 logs rejected)", LogPartialSuccessError(15, "what happened")) | ||||
| } | ||||
							
								
								
									
										7
									
								
								exporters/otlp/otlplog/otlploghttp/internal/version.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								exporters/otlp/otlplog/otlploghttp/internal/version.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| // Copyright The OpenTelemetry Authors | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
|  | ||||
| package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal" | ||||
|  | ||||
| // Version is the current release version of the OpenTelemetry OTLP over HTTP/protobuf logs exporter in use. | ||||
| const Version = "0.14.0" | ||||
							
								
								
									
										36
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| # Experimental Features | ||||
|  | ||||
| The `otlploghttp` exporter contains features that have not yet stabilized in the OpenTelemetry specification. | ||||
| These features are added to the `otlploghttp` 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 `otlploghttp` 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. | ||||
							
								
								
									
										22
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/observ.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/observ.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // Copyright The OpenTelemetry Authors | ||||
| // SPDX-License-Identifier: Apache-2.0 | ||||
|  | ||||
| package x // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/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 | ||||
| 	}, | ||||
| ) | ||||
							
								
								
									
										21
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/observ_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/observ_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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))) | ||||
| } | ||||
							
								
								
									
										58
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/x.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/x.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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/otlp/otlplog/otlploghttp]. | ||||
| package x // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/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 | ||||
| } | ||||
							
								
								
									
										75
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/x_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								exporters/otlp/otlplog/otlploghttp/internal/x/x_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| // Code generated by gotmpl. DO NOT MODIFY. | ||||
| // source: internal/shared/x/x_text.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") | ||||
| 	} | ||||
| } | ||||
| @@ -58,3 +58,6 @@ modules: | ||||
|   go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp: | ||||
|     version-refs: | ||||
|       - ./exporters/otlp/otlptrace/otlptracehttp/internal/version.go | ||||
|   go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp: | ||||
|     version-refs: | ||||
|       - ./exporters/otlp/otlplog/otlploghttp/internal/version.go | ||||
		Reference in New Issue
	
	Block a user