From ed73a668a46a8c73a573a2314cbcfafa191200ab Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 24 Jul 2025 11:22:13 +0800 Subject: [PATCH 01/42] add the otlplogrpc selfObservaility --- exporters/otlp/otlplog/otlploggrpc/client.go | 56 +++++++++++++++++-- .../otlp/otlplog/otlploggrpc/client_test.go | 18 ++++++ exporters/otlp/otlplog/otlploggrpc/go.mod | 2 + exporters/otlp/otlplog/otlploggrpc/go.sum | 7 +++ .../otlp/otlplog/otlploggrpc/internal/x/x.go | 51 +++++++++++++++++ 5 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/x/x.go diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 1add3f33330..23770480e78 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -9,6 +9,15 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" + collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" + logpb "go.opentelemetry.io/proto/otlp/logs/v1" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/backoff" @@ -18,11 +27,6 @@ import ( "google.golang.org/grpc/encoding/gzip" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" - collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" - logpb "go.opentelemetry.io/proto/otlp/logs/v1" ) // The methods of this type are not expected to be called concurrently. @@ -38,6 +42,11 @@ type client struct { ourConn bool conn *grpc.ClientConn lsc collogpb.LogsServiceClient + + selfObservabilityEnabled bool + logInflightMetric otelconv.SDKExporterLogInflight + logExportedMetric otelconv.SDKExporterLogExported + logExportedDurationMetric otelconv.SDKExporterOperationDuration } // Used for testing. @@ -71,10 +80,33 @@ func newClient(cfg config) (*client, error) { } c.lsc = collogpb.NewLogsServiceClient(c.conn) - + c.InitSelfObservability() return c, nil } +func (c *client) InitSelfObservability() { + if !x.SelfObservability.Enable() { + return + } + + c.selfObservabilityEnabled = true + mp := otel.GetMeterProvider() + m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", + metric.WithInstrumentationVersion(sdk.Version()), + metric.WithSchemaURL(semconv.SchemaURL)) + + var err error + if c.logInflightMetric, err = otelconv.NewSDKExporterLogInflight(m); err != nil { + otel.Handle(err) + } + if c.logExportedMetric, err = otelconv.NewSDKExporterLogExported(m); err != nil { + otel.Handle(err) + } + if c.logExportedDurationMetric, err = otelconv.NewSDKExporterOperationDuration(m); err != nil { + otel.Handle(err) + } +} + func newGRPCDialOptions(cfg config) []grpc.DialOption { userAgent := "OTel Go OTLP over gRPC logs exporter/" + Version() dialOpts := []grpc.DialOption{grpc.WithUserAgent(userAgent)} @@ -132,9 +164,21 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error defer cancel() return c.requestFunc(ctx, func(ctx context.Context) error { + var begin time.Time + if c.selfObservabilityEnabled { + begin = time.Now() + c.logInflightMetric.Int64UpDownCounter.Add(ctx, 1) + } resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ ResourceLogs: rl, }) + + if c.selfObservabilityEnabled { + c.logExportedMetric.Add(ctx, 1) + duration := time.Since(begin) + c.logExportedDurationMetric.Record(ctx, duration.Seconds()) + } + if resp != nil && resp.PartialSuccess != nil { msg := resp.PartialSuccess.GetErrorMessage() n := resp.PartialSuccess.GetRejectedLogRecords() diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 2b5cec5d850..ac462b29c83 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/metric/metricdata" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -601,3 +602,20 @@ func TestConfig(t *testing.T) { assert.Equal(t, []string{headers[key]}, got[key]) }) } + +func TestSelfObservability(t *testing.T) { + testCases := []struct { + name string + test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) + }{ + { + name: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + }) + } +} diff --git a/exporters/otlp/otlplog/otlploggrpc/go.mod b/exporters/otlp/otlplog/otlploggrpc/go.mod index b4d6329ff33..1441e7d62d4 100644 --- a/exporters/otlp/otlplog/otlploggrpc/go.mod +++ b/exporters/otlp/otlplog/otlploggrpc/go.mod @@ -49,4 +49,6 @@ replace go.opentelemetry.io/otel/trace => ../../../../trace replace go.opentelemetry.io/otel/metric => ../../../../metric +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric + replace go.opentelemetry.io/otel/sdk/log/logtest => ../../../../sdk/log/logtest diff --git a/exporters/otlp/otlplog/otlploggrpc/go.sum b/exporters/otlp/otlplog/otlploggrpc/go.sum index b3c3f5c4e57..a08d075c81b 100644 --- a/exporters/otlp/otlplog/otlploggrpc/go.sum +++ b/exporters/otlp/otlplog/otlploggrpc/go.sum @@ -27,8 +27,15 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go b/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go new file mode 100644 index 00000000000..c4dca03ee8f --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package x contains support for OTel SDK experimental features. +// +// This package should only be used for features defined in the specification. +// It should not be used for experiments or new project ideas. + +package x + +import ( + "os" + "strings" +) + +type Feature[T any] struct { + key string + parse func(v string) (T, bool) +} + +func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { + const envKeyRoot = "OTEL_GO_X_" + return Feature[T]{ + key: envKeyRoot + suffix, + parse: parse, + } +} + +var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) { + if strings.ToLower(v) == "true" { + return v, true + } + return "", false +}) + +func (f Feature[T]) Key() string { + return f.key +} + +func (f Feature[T]) Lookup() (v T, ok bool) { + vRaw := os.Getenv(f.key) + if vRaw == "" { + return v, ok + } + return f.parse(vRaw) +} + +func (f Feature[T]) Enable() bool { + _, ok := f.Lookup() + return ok +} From 87f64f324cf7fbecda82776c9f0ede7fb42009dc Mon Sep 17 00:00:00 2001 From: yumosx Date: Fri, 25 Jul 2025 18:03:03 +0800 Subject: [PATCH 02/42] Add self-observability support for otlplog grpc exporter --- .../otlp/otlplog/otlploggrpc/client_test.go | 98 ++++++++++++++++++- exporters/otlp/otlplog/otlploggrpc/go.mod | 5 +- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index ac462b29c83..3bdc5d4b744 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -14,7 +14,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -27,7 +33,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/log" - semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" cpb "go.opentelemetry.io/proto/otlp/common/v1" lpb "go.opentelemetry.io/proto/otlp/logs/v1" @@ -609,13 +615,97 @@ func TestSelfObservability(t *testing.T) { test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) }{ { - name: "", + name: "test self observability", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKExporterLogInflight{}.Name(), + Description: otelconv.SDKExporterLogInflight{}.Description(), + Unit: otelconv.SDKExporterLogInflight{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKExporterLogExported{}.Name(), + Description: otelconv.SDKExporterLogExported{}.Description(), + Unit: otelconv.SDKExporterLogExported{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKExporterOperationDuration{}.Name(), + Description: otelconv.SDKExporterOperationDuration{}.Description(), + Unit: otelconv.SDKExporterOperationDuration{}.Unit(), + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attribute.NewSet(), + Count: 1, + Bounds: []float64{}, + BucketCounts: []uint64{1}, + }, + }, + }, + }, + }, + } + + ctx := context.Background() + client, coll := clientFactory(t, nil) + + require.NoError(t, client.UploadLogs(ctx, resourceLogs)) + require.NoError(t, client.Shutdown(ctx)) + got := coll.Collect().Dump() + require.Len(t, got, 1, "upload of one ResourceLogs") + diff := cmp.Diff(got[0], resourceLogs[0], cmp.Comparer(proto.Equal)) + if diff != "" { + t.Fatalf("unexpected ResourceLogs:\n%s", diff) + } + + g := scopeMetrics() + metricdatatest.AssertEqual(t, want, g, metricdatatest.IgnoreTimestamp()) + }, }, } - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - + t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True") + prev := otel.GetMeterProvider() + defer otel.SetMeterProvider(prev) + r := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(r)) + otel.SetMeterProvider(mp) + + scopeMetrics := func() metricdata.ScopeMetrics { + var got metricdata.ResourceMetrics + err := r.Collect(context.Background(), &got) + require.NoError(t, err) + require.Len(t, got.ScopeMetrics, 1) + return got.ScopeMetrics[0] + } + tc.test(t, scopeMetrics) }) } } diff --git a/exporters/otlp/otlplog/otlploggrpc/go.mod b/exporters/otlp/otlplog/otlploggrpc/go.mod index d5b60bd2a7f..0a90fd58e78 100644 --- a/exporters/otlp/otlplog/otlploggrpc/go.mod +++ b/exporters/otlp/otlplog/otlploggrpc/go.mod @@ -11,9 +11,11 @@ require ( github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel/log v0.13.0 + go.opentelemetry.io/otel/metric v1.37.0 go.opentelemetry.io/otel/sdk v1.37.0 go.opentelemetry.io/otel/sdk/log v0.13.0 go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 + go.opentelemetry.io/otel/sdk/metric v1.37.0 go.opentelemetry.io/otel/trace v1.37.0 go.opentelemetry.io/proto/otlp v1.7.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 @@ -29,7 +31,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect @@ -49,8 +50,6 @@ replace go.opentelemetry.io/otel/trace => ../../../../trace replace go.opentelemetry.io/otel/metric => ../../../../metric -replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric - replace go.opentelemetry.io/otel/sdk/log/logtest => ../../../../sdk/log/logtest replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric From 56be45b2c9bce3344b69ff178542c6141c471856 Mon Sep 17 00:00:00 2001 From: yumosx Date: Fri, 25 Jul 2025 18:08:09 +0800 Subject: [PATCH 03/42] commit --- exporters/otlp/otlplog/otlploggrpc/client.go | 137 ++++++++-- .../otlp/otlplog/otlploggrpc/client_test.go | 237 ++++++++++++++++-- .../otlp/otlplog/otlploggrpc/internal/x/x.go | 23 +- 3 files changed, 358 insertions(+), 39 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 23770480e78..d6c4d29c2cd 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -7,17 +7,11 @@ import ( "context" "errors" "fmt" + "net" + "strconv" + "sync/atomic" "time" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/sdk" - semconv "go.opentelemetry.io/otel/semconv/v1.36.0" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" - collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" - logpb "go.opentelemetry.io/proto/otlp/logs/v1" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/backoff" @@ -27,8 +21,29 @@ import ( "google.golang.org/grpc/encoding/gzip" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + + "go.opentelemetry.io/otel/attribute" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" + collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" + logpb "go.opentelemetry.io/proto/otlp/logs/v1" ) +var ( + componentType = otelconv.ComponentTypeOtlpGRPCLogExporter + exporterInstanceCounter int64 +) + +func getExporterInstanceName() string { + return fmt.Sprintf("%s/%d", componentType, exporterInstanceCounter) +} + // The methods of this type are not expected to be called concurrently. type client struct { metadata metadata.MD @@ -85,11 +100,12 @@ func newClient(cfg config) (*client, error) { } func (c *client) InitSelfObservability() { - if !x.SelfObservability.Enable() { + if !x.SelfObservability.Enabled() { return } c.selfObservabilityEnabled = true + atomic.AddInt64(&exporterInstanceCounter, 1) mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", metric.WithInstrumentationVersion(sdk.Version()), @@ -156,6 +172,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error select { case <-ctx.Done(): // Do not upload if the context is already expired. + c.recordLogInflightMetric(ctx, attribute.String("error.type", ctx.Err().Error())) return ctx.Err() default: } @@ -164,19 +181,19 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error defer cancel() return c.requestFunc(ctx, func(ctx context.Context) error { - var begin time.Time + var ( + begin time.Time + duration time.Duration + ) if c.selfObservabilityEnabled { begin = time.Now() - c.logInflightMetric.Int64UpDownCounter.Add(ctx, 1) } resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ ResourceLogs: rl, }) if c.selfObservabilityEnabled { - c.logExportedMetric.Add(ctx, 1) - duration := time.Since(begin) - c.logExportedDurationMetric.Record(ctx, duration.Seconds()) + duration = time.Since(begin) } if resp != nil && resp.PartialSuccess != nil { @@ -184,18 +201,108 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error n := resp.PartialSuccess.GetRejectedLogRecords() if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) + c.recordLogInflightMetric(ctx, attribute.String("error.type", err.Error())) + c.recordLogExportedMetric(ctx, attribute.String("error.type", err.Error())) + c.recordLogExportedDurationMetric( + ctx, + duration.Seconds(), + c.logExportedDurationMetric.AttrRPCGRPCStatusCode( + otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), + ), + c.logExportedDurationMetric.AttrErrorType(otelconv.ErrorTypeAttr(err.Error())), + ) otel.Handle(err) + return nil } } // nil is converted to OK. if status.Code(err) == codes.OK { // Success. + c.recordLogInflightMetric(ctx) + c.recordLogExportedMetric(ctx) + c.recordLogExportedDurationMetric(ctx, duration.Seconds()) return nil } + c.recordLogInflightMetric(ctx, attribute.String("error.type", err.Error())) + c.recordLogExportedMetric(ctx, attribute.String("error.type", err.Error())) + c.recordLogExportedDurationMetric( + ctx, + duration.Seconds(), + c.logExportedDurationMetric.AttrRPCGRPCStatusCode( + otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), + ), + c.logExportedDurationMetric.AttrErrorType(otelconv.ErrorTypeAttr(err.Error())), + ) return err }) } +func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attribute.KeyValue) { + if !c.selfObservabilityEnabled { + return + } + attrs := []attribute.KeyValue{ + c.logInflightMetric.AttrComponentName(getExporterInstanceName()), + c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), + c.logInflightMetric.AttrServerAddress(c.conn.Target()), + c.logInflightMetric.AttrServerPort(c.getPort()), + } + + attrs = append(attrs, extraAttrs...) + + c.logInflightMetric.Add(ctx, 1, attrs...) +} + +func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attribute.KeyValue) { + if !c.selfObservabilityEnabled { + return + } + + attrs := []attribute.KeyValue{ + c.logExportedMetric.AttrComponentName(getExporterInstanceName()), + c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), + c.logExportedMetric.AttrServerAddress(c.conn.Target()), + c.logExportedMetric.AttrServerPort(c.getPort()), + } + + attrs = append(attrs, extraAttrs...) + c.logExportedMetric.Add(ctx, 1, attrs...) +} + +func (c *client) recordLogExportedDurationMetric( + ctx context.Context, + duration float64, + extraAttrs ...attribute.KeyValue, +) { + if !c.selfObservabilityEnabled { + return + } + + attrs := []attribute.KeyValue{ + c.logExportedDurationMetric.AttrComponentName(getExporterInstanceName()), + c.logExportedDurationMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), + c.logExportedDurationMetric.AttrServerAddress(c.conn.Target()), + c.logExportedMetric.AttrServerPort(c.getPort()), + } + + attrs = append(attrs, extraAttrs...) + + c.logExportedDurationMetric.Record(ctx, duration, attrs...) +} + +func (c *client) getPort() int { + _, p, err := net.SplitHostPort(c.conn.Target()) + if err != nil { + otel.Handle(err) + } + + port, err := strconv.Atoi(p) + if err != nil { + otel.Handle(err) + } + return port +} + // Shutdown shuts down the client, freeing all resources. // // Any active connections to a remote endpoint are closed if they were created diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 3bdc5d4b744..4d8bab66f8d 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -14,13 +14,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/sdk" - "go.opentelemetry.io/otel/sdk/instrumentation" - "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -31,6 +24,14 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/log" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" @@ -615,8 +616,10 @@ func TestSelfObservability(t *testing.T) { test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) }{ { - name: "test self observability", + name: "upload success", test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + ctx := context.Background() + client, coll := clientFactory(t, nil) want := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -632,8 +635,17 @@ func TestSelfObservability(t *testing.T) { Temporality: metricdata.CumulativeTemporality, DataPoints: []metricdata.DataPoint[int64]{ { - Attributes: attribute.NewSet(), - Value: 1, + Attributes: attribute.NewSet( + otelconv.SDKExporterLogInflight{}.AttrComponentName( + fmt.Sprintf("%s/%d", componentType, 1), + ), + otelconv.SDKExporterLogInflight{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), + otelconv.SDKExporterLogInflight{}.AttrServerPort(client.getPort()), + ), + Value: 1, }, }, }, @@ -647,8 +659,17 @@ func TestSelfObservability(t *testing.T) { IsMonotonic: true, DataPoints: []metricdata.DataPoint[int64]{ { - Attributes: attribute.NewSet(), - Value: 1, + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName( + fmt.Sprintf("%s/%d", componentType, 1), + ), + otelconv.SDKExporterLogExported{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), + otelconv.SDKExporterLogExported{}.AttrServerPort(client.getPort()), + ), + Value: 1, }, }, }, @@ -661,20 +682,25 @@ func TestSelfObservability(t *testing.T) { Temporality: metricdata.CumulativeTemporality, DataPoints: []metricdata.HistogramDataPoint[float64]{ { - Attributes: attribute.NewSet(), - Count: 1, - Bounds: []float64{}, - BucketCounts: []uint64{1}, + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName( + fmt.Sprintf("%s/%d", componentType, 1), + ), + otelconv.SDKExporterOperationDuration{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterOperationDuration{}.AttrServerAddress( + client.conn.Target(), + ), + otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.getPort()), + ), + Count: 1, }, }, }, }, }, } - - ctx := context.Background() - client, coll := clientFactory(t, nil) - require.NoError(t, client.UploadLogs(ctx, resourceLogs)) require.NoError(t, client.Shutdown(ctx)) got := coll.Collect().Dump() @@ -683,11 +709,157 @@ func TestSelfObservability(t *testing.T) { if diff != "" { t.Fatalf("unexpected ResourceLogs:\n%s", diff) } - g := scopeMetrics() + normalizeMetrics(&g) metricdatatest.AssertEqual(t, want, g, metricdatatest.IgnoreTimestamp()) }, }, + { + name: "PartialSuccess", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + const n, msg = 2, "bad data" + rCh := make(chan exportResult, 1) + rCh <- exportResult{ + Response: &collogpb.ExportLogsServiceResponse{ + PartialSuccess: &collogpb.ExportLogsPartialSuccess{ + RejectedLogRecords: n, + ErrorMessage: msg, + }, + }, + } + ctx := context.Background() + client, _ := clientFactory(t, rCh) + + wantMetrics := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKExporterLogInflight{}.Name(), + Description: otelconv.SDKExporterLogInflight{}.Description(), + Unit: otelconv.SDKExporterLogInflight{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogInflight{}.AttrComponentName( + fmt.Sprintf("%s/%d", componentType, 2), + ), + otelconv.SDKExporterLogInflight{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), + otelconv.SDKExporterLogInflight{}.AttrServerPort(client.getPort()), + attribute.String( + "error.type", + fmt.Sprintf( + "OTLP partial success: %s (%d log records rejected)", + msg, + n, + ), + ), + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKExporterLogExported{}.Name(), + Description: otelconv.SDKExporterLogExported{}.Description(), + Unit: otelconv.SDKExporterLogExported{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName( + fmt.Sprintf("%s/%d", componentType, 2), + ), + otelconv.SDKExporterLogExported{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), + otelconv.SDKExporterLogExported{}.AttrServerPort(client.getPort()), + attribute.String( + "error.type", + fmt.Sprintf( + "OTLP partial success: %s (%d log records rejected)", + msg, + n, + ), + ), + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKExporterOperationDuration{}.Name(), + Description: otelconv.SDKExporterOperationDuration{}.Description(), + Unit: otelconv.SDKExporterOperationDuration{}.Unit(), + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName( + fmt.Sprintf("%s/%d", componentType, 2), + ), + otelconv.SDKExporterOperationDuration{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterOperationDuration{}.AttrServerAddress( + client.conn.Target(), + ), + otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.getPort()), + attribute.String( + "error.type", + fmt.Sprintf( + "OTLP partial success: %s (%d log records rejected)", + msg, + n, + ), + ), + otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( + otelconv.RPCGRPCStatusCodeAttr( + status.Code(fmt.Errorf("%s (%d log records rejected)", msg, n)), + ), + ), + ), + Count: 1, + }, + }, + }, + }, + }, + } + + defer func(orig otel.ErrorHandler) { + otel.SetErrorHandler(orig) + }(otel.GetErrorHandler()) + + var errs []error + eh := otel.ErrorHandlerFunc(func(e error) { errs = append(errs, e) }) + otel.SetErrorHandler(eh) + + require.NoError(t, client.UploadLogs(ctx, resourceLogs)) + + require.Len(t, errs, 1) + want := fmt.Sprintf("%s (%d log records rejected)", msg, n) + assert.ErrorContains(t, errs[0], want) + + g := scopeMetrics() + normalizeMetrics(&g) + metricdatatest.AssertEqual(t, wantMetrics, g, metricdatatest.IgnoreTimestamp()) + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -709,3 +881,26 @@ func TestSelfObservability(t *testing.T) { }) } } + +func normalizeMetrics(scopeMetrics *metricdata.ScopeMetrics) { + for i := range scopeMetrics.Metrics { + m := &scopeMetrics.Metrics[i] + if data, ok := m.Data.(metricdata.Histogram[float64]); ok { + name := otelconv.SDKExporterOperationDuration{}.Name() + if m.Name != name { + break + } + for j := range data.DataPoints { + dp := &data.DataPoints[j] + dp.StartTime = time.Time{} + dp.Time = time.Time{} + dp.Min = metricdata.Extrema[float64]{} + dp.Max = metricdata.Extrema[float64]{} + dp.Bounds = nil + dp.BucketCounts = nil + dp.Sum = 0 + } + m.Data = data + } + } +} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go b/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go index c4dca03ee8f..24251f82f98 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go @@ -5,14 +5,15 @@ // // This package should only be used for features defined in the specification. // It should not be used for experiments or new project ideas. - -package x +package x // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" import ( "os" "strings" ) +// Feature is an experimental feature control flag. It provides a uniform way +// to interact with these feature flags and parse their values. type Feature[T any] struct { key string parse func(v string) (T, bool) @@ -26,6 +27,12 @@ func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { } } +// SelfObservability is an experimental feature flag that determines if SDK +// self-observability metrics are enabled. +// +// To enable this feature set the OTEL_GO_X_SELF_OBSERVABILITY environment variable +// to the case-insensitive string value of "true" (i.e. "True" and "TRUE" +// will also enable this). var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) { if strings.ToLower(v) == "true" { return v, true @@ -33,11 +40,20 @@ var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, return "", false }) +// Key returns the environment variable key that needs to be set to enable the +// feature. func (f Feature[T]) Key() string { return f.key } +// Lookup returns the user configured value for the feature and true if the +// user has enabled the feature. Otherwise, if the feature is not enabled, a +// zero-value and false are returned. func (f Feature[T]) Lookup() (v T, ok bool) { + // https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value + // + // > The SDK MUST interpret an empty value of an environment variable the + // > same way as when the variable is unset. vRaw := os.Getenv(f.key) if vRaw == "" { return v, ok @@ -45,7 +61,8 @@ func (f Feature[T]) Lookup() (v T, ok bool) { return f.parse(vRaw) } -func (f Feature[T]) Enable() bool { +// Enabled reports whether the feature is enabled. +func (f Feature[T]) Enabled() bool { _, ok := f.Lookup() return ok } From 29615562d67180761571eefbbb5377b41c8c14c1 Mon Sep 17 00:00:00 2001 From: yumosx Date: Fri, 25 Jul 2025 19:53:51 +0800 Subject: [PATCH 04/42] add x_test.go and update selfObsever --- exporters/otlp/otlplog/otlploggrpc/client.go | 24 ++++---- .../otlplog/otlploggrpc/internal/x/x_test.go | 60 +++++++++++++++++++ 2 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/x/x_test.go diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index d6c4d29c2cd..805e88f75d4 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -95,11 +95,11 @@ func newClient(cfg config) (*client, error) { } c.lsc = collogpb.NewLogsServiceClient(c.conn) - c.InitSelfObservability() + c.initSelfObservability() return c, nil } -func (c *client) InitSelfObservability() { +func (c *client) initSelfObservability() { if !x.SelfObservability.Enabled() { return } @@ -172,7 +172,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error select { case <-ctx.Done(): // Do not upload if the context is already expired. - c.recordLogInflightMetric(ctx, attribute.String("error.type", ctx.Err().Error())) + c.recordLogInflightMetric(context.Background(), attribute.String("error.type", ctx.Err().Error())) return ctx.Err() default: } @@ -201,10 +201,10 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error n := resp.PartialSuccess.GetRejectedLogRecords() if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) - c.recordLogInflightMetric(ctx, attribute.String("error.type", err.Error())) - c.recordLogExportedMetric(ctx, attribute.String("error.type", err.Error())) + c.recordLogInflightMetric(context.Background(), attribute.String("error.type", err.Error())) + c.recordLogExportedMetric(context.Background(), attribute.String("error.type", err.Error())) c.recordLogExportedDurationMetric( - ctx, + context.Background(), duration.Seconds(), c.logExportedDurationMetric.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), @@ -218,15 +218,15 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error // nil is converted to OK. if status.Code(err) == codes.OK { // Success. - c.recordLogInflightMetric(ctx) - c.recordLogExportedMetric(ctx) - c.recordLogExportedDurationMetric(ctx, duration.Seconds()) + c.recordLogInflightMetric(context.Background()) + c.recordLogExportedMetric(context.Background()) + c.recordLogExportedDurationMetric(context.Background(), duration.Seconds()) return nil } - c.recordLogInflightMetric(ctx, attribute.String("error.type", err.Error())) - c.recordLogExportedMetric(ctx, attribute.String("error.type", err.Error())) + c.recordLogInflightMetric(context.Background(), attribute.String("error.type", err.Error())) + c.recordLogExportedMetric(context.Background(), attribute.String("error.type", err.Error())) c.recordLogExportedDurationMetric( - ctx, + context.Background(), duration.Seconds(), c.logExportedDurationMetric.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/x_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/x/x_test.go new file mode 100644 index 00000000000..bf386209147 --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/x_test.go @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSelfObservability(t *testing.T) { + const key = "OTEL_GO_X_SELF_OBSERVABILITY" + require.Equal(t, key, SelfObservability.Key()) + + t.Run("-100", run(setenv(key, "-100"), assertDisabled(SelfObservability))) + t.Run("100", run(setenv(key, "100"), assertDisabled(SelfObservability))) + t.Run("true", run(setenv(key, "true"), assertEnabled(SelfObservability, "true"))) + t.Run("True", run(setenv(key, "True"), assertEnabled(SelfObservability, "True"))) + t.Run("false", run(setenv(key, "false"), assertDisabled(SelfObservability))) + t.Run("empty", run(assertDisabled(SelfObservability))) +} + +func run(steps ...func(*testing.T)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + for _, step := range steps { + step(t) + } + } +} + +func setenv(k, v string) func(t *testing.T) { //nolint:unparam // This is a reusable test utility function. + return func(t *testing.T) { t.Setenv(k, v) } +} + +func assertEnabled[T any](f Feature[T], want T) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + assert.True(t, f.Enabled(), "not enabled") + + v, ok := f.Lookup() + assert.True(t, ok, "Lookup state") + assert.Equal(t, want, v, "Lookup value") + } +} + +func assertDisabled[T any](f Feature[T]) func(*testing.T) { + var zero T + return func(t *testing.T) { + t.Helper() + + assert.False(t, f.Enabled(), "enabled") + + v, ok := f.Lookup() + assert.False(t, ok, "Lookup state") + assert.Equal(t, zero, v, "Lookup value") + } +} From 97a7d77f2b268301dd30511098a1ba87b4dfcb88 Mon Sep 17 00:00:00 2001 From: yumosx Date: Sat, 26 Jul 2025 01:06:36 +0800 Subject: [PATCH 05/42] - compute post on initialization - use atomic.Int64 - use ErrorType --- exporters/otlp/otlplog/otlploggrpc/client.go | 36 +++++------ .../otlp/otlplog/otlploggrpc/client_test.go | 61 +++++-------------- 2 files changed, 35 insertions(+), 62 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 805e88f75d4..baf5c8630c5 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -22,9 +22,8 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" "go.opentelemetry.io/otel/metric" @@ -37,11 +36,11 @@ import ( var ( componentType = otelconv.ComponentTypeOtlpGRPCLogExporter - exporterInstanceCounter int64 + exporterInstanceCounter atomic.Int64 ) -func getExporterInstanceName() string { - return fmt.Sprintf("%s/%d", componentType, exporterInstanceCounter) +func getComponentName() string { + return fmt.Sprintf("%s/%d", componentType, exporterInstanceCounter.Load()) } // The methods of this type are not expected to be called concurrently. @@ -58,6 +57,8 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient + port int + componentName string selfObservabilityEnabled bool logInflightMetric otelconv.SDKExporterLogInflight logExportedMetric otelconv.SDKExporterLogExported @@ -103,9 +104,10 @@ func (c *client) initSelfObservability() { if !x.SelfObservability.Enabled() { return } - + exporterInstanceCounter.Add(1) c.selfObservabilityEnabled = true - atomic.AddInt64(&exporterInstanceCounter, 1) + c.port = c.getPort() + c.componentName = getComponentName() mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", metric.WithInstrumentationVersion(sdk.Version()), @@ -172,7 +174,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error select { case <-ctx.Done(): // Do not upload if the context is already expired. - c.recordLogInflightMetric(context.Background(), attribute.String("error.type", ctx.Err().Error())) + c.recordLogInflightMetric(context.Background(), semconv.ErrorType(ctx.Err())) return ctx.Err() default: } @@ -201,15 +203,15 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error n := resp.PartialSuccess.GetRejectedLogRecords() if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) - c.recordLogInflightMetric(context.Background(), attribute.String("error.type", err.Error())) - c.recordLogExportedMetric(context.Background(), attribute.String("error.type", err.Error())) + c.recordLogInflightMetric(context.Background(), semconv.ErrorType(err)) + c.recordLogExportedMetric(context.Background(), semconv.ErrorType(err)) c.recordLogExportedDurationMetric( context.Background(), duration.Seconds(), c.logExportedDurationMetric.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), ), - c.logExportedDurationMetric.AttrErrorType(otelconv.ErrorTypeAttr(err.Error())), + semconv.ErrorType(err), ) otel.Handle(err) return nil @@ -223,8 +225,8 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error c.recordLogExportedDurationMetric(context.Background(), duration.Seconds()) return nil } - c.recordLogInflightMetric(context.Background(), attribute.String("error.type", err.Error())) - c.recordLogExportedMetric(context.Background(), attribute.String("error.type", err.Error())) + c.recordLogInflightMetric(context.Background(), semconv.ErrorType(err)) + c.recordLogExportedMetric(context.Background(), semconv.ErrorType(err)) c.recordLogExportedDurationMetric( context.Background(), duration.Seconds(), @@ -242,10 +244,10 @@ func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attr return } attrs := []attribute.KeyValue{ - c.logInflightMetric.AttrComponentName(getExporterInstanceName()), + c.logInflightMetric.AttrComponentName(c.componentName), c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), c.logInflightMetric.AttrServerAddress(c.conn.Target()), - c.logInflightMetric.AttrServerPort(c.getPort()), + c.logInflightMetric.AttrServerPort(c.port), } attrs = append(attrs, extraAttrs...) @@ -259,7 +261,7 @@ func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attr } attrs := []attribute.KeyValue{ - c.logExportedMetric.AttrComponentName(getExporterInstanceName()), + c.logExportedMetric.AttrComponentName(c.componentName), c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), c.logExportedMetric.AttrServerAddress(c.conn.Target()), c.logExportedMetric.AttrServerPort(c.getPort()), @@ -279,7 +281,7 @@ func (c *client) recordLogExportedDurationMetric( } attrs := []attribute.KeyValue{ - c.logExportedDurationMetric.AttrComponentName(getExporterInstanceName()), + c.logExportedDurationMetric.AttrComponentName(c.componentName), c.logExportedDurationMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), c.logExportedDurationMetric.AttrServerAddress(c.conn.Target()), c.logExportedMetric.AttrServerPort(c.getPort()), diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 4d8bab66f8d..d56621548dc 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -636,14 +636,12 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogInflight{}.AttrComponentName( - fmt.Sprintf("%s/%d", componentType, 1), - ), + otelconv.SDKExporterLogInflight{}.AttrComponentName(client.componentName), otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogInflight{}.AttrServerPort(client.getPort()), + otelconv.SDKExporterLogInflight{}.AttrServerPort(client.port), ), Value: 1, }, @@ -660,14 +658,12 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName( - fmt.Sprintf("%s/%d", componentType, 1), - ), + otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogExported{}.AttrServerPort(client.getPort()), + otelconv.SDKExporterLogExported{}.AttrServerPort(client.port), ), Value: 1, }, @@ -683,16 +679,14 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName( - fmt.Sprintf("%s/%d", componentType, 1), - ), + otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), otelconv.SDKExporterOperationDuration{}.AttrServerAddress( client.conn.Target(), ), - otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.getPort()), + otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.port), ), Count: 1, }, @@ -730,6 +724,7 @@ func TestSelfObservability(t *testing.T) { ctx := context.Background() client, _ := clientFactory(t, rCh) + wantErr := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -746,22 +741,13 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogInflight{}.AttrComponentName( - fmt.Sprintf("%s/%d", componentType, 2), - ), + otelconv.SDKExporterLogInflight{}.AttrComponentName(client.componentName), otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogInflight{}.AttrServerPort(client.getPort()), - attribute.String( - "error.type", - fmt.Sprintf( - "OTLP partial success: %s (%d log records rejected)", - msg, - n, - ), - ), + otelconv.SDKExporterLogInflight{}.AttrServerPort(client.port), + semconv.ErrorType(wantErr), ), Value: 1, }, @@ -785,15 +771,8 @@ func TestSelfObservability(t *testing.T) { otelconv.ComponentTypeOtlpGRPCLogExporter, ), otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogExported{}.AttrServerPort(client.getPort()), - attribute.String( - "error.type", - fmt.Sprintf( - "OTLP partial success: %s (%d log records rejected)", - msg, - n, - ), - ), + otelconv.SDKExporterLogExported{}.AttrServerPort(client.port), + semconv.ErrorType(wantErr), ), Value: 1, }, @@ -809,9 +788,7 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName( - fmt.Sprintf("%s/%d", componentType, 2), - ), + otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), @@ -819,19 +796,12 @@ func TestSelfObservability(t *testing.T) { client.conn.Target(), ), otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.getPort()), - attribute.String( - "error.type", - fmt.Sprintf( - "OTLP partial success: %s (%d log records rejected)", - msg, - n, - ), - ), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr( - status.Code(fmt.Errorf("%s (%d log records rejected)", msg, n)), + status.Code(wantErr), ), ), + semconv.ErrorType(wantErr), ), Count: 1, }, @@ -858,6 +828,7 @@ func TestSelfObservability(t *testing.T) { g := scopeMetrics() normalizeMetrics(&g) metricdatatest.AssertEqual(t, wantMetrics, g, metricdatatest.IgnoreTimestamp()) + exporterInstanceCounter.Add(-1) }, }, } From 030120ce7ce448a364c6806e0f95f713fb33c0c6 Mon Sep 17 00:00:00 2001 From: yumosx Date: Sat, 26 Jul 2025 01:12:39 +0800 Subject: [PATCH 06/42] fix the data race --- exporters/otlp/otlplog/otlploggrpc/client.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index baf5c8630c5..a819d255f43 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -39,10 +39,6 @@ var ( exporterInstanceCounter atomic.Int64 ) -func getComponentName() string { - return fmt.Sprintf("%s/%d", componentType, exporterInstanceCounter.Load()) -} - // The methods of this type are not expected to be called concurrently. type client struct { metadata metadata.MD @@ -104,10 +100,12 @@ func (c *client) initSelfObservability() { if !x.SelfObservability.Enabled() { return } - exporterInstanceCounter.Add(1) + + counter := exporterInstanceCounter.Add(1) c.selfObservabilityEnabled = true c.port = c.getPort() - c.componentName = getComponentName() + c.componentName = fmt.Sprintf("%s/%d", componentType, counter) + mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", metric.WithInstrumentationVersion(sdk.Version()), From 0c7a55bcb4e88b74ae74a63b1eded1cd06efbacd Mon Sep 17 00:00:00 2001 From: yumosx Date: Sat, 26 Jul 2025 11:08:42 +0800 Subject: [PATCH 07/42] updated --- exporters/otlp/otlplog/otlploggrpc/client.go | 4 ++-- exporters/otlp/otlplog/otlploggrpc/client_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index a819d255f43..dc5fcb6e14a 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -262,7 +262,7 @@ func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attr c.logExportedMetric.AttrComponentName(c.componentName), c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), c.logExportedMetric.AttrServerAddress(c.conn.Target()), - c.logExportedMetric.AttrServerPort(c.getPort()), + c.logExportedMetric.AttrServerPort(c.port), } attrs = append(attrs, extraAttrs...) @@ -282,7 +282,7 @@ func (c *client) recordLogExportedDurationMetric( c.logExportedDurationMetric.AttrComponentName(c.componentName), c.logExportedDurationMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), c.logExportedDurationMetric.AttrServerAddress(c.conn.Target()), - c.logExportedMetric.AttrServerPort(c.getPort()), + c.logExportedMetric.AttrServerPort(c.port), } attrs = append(attrs, extraAttrs...) diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index d56621548dc..1d78b72beb6 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -795,7 +795,7 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterOperationDuration{}.AttrServerAddress( client.conn.Target(), ), - otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.getPort()), + otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.port), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr( status.Code(wantErr), From be6242ebeae6a3ddbc38945cdd38cd3573606a4c Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Jul 2025 21:14:16 +0800 Subject: [PATCH 08/42] add changlog entry and readme.md --- CHANGELOG.md | 2 ++ .../otlplog/otlploggrpc/internal/x/READEME.md | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/x/READEME.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fcc8a608ab..5ee341ad1ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032) - Add experimental self-observability span metrics in `go.opentelemetry.io/otel/sdk/trace`. Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027) +- Add experimental self-observability metrics `exporter.log.inflight`, `exporter.log.exported`, and `exporter.operation.duration` in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. + Check the `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x` package documentation for more information. (#7084) ### Changed diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/READEME.md b/exporters/otlp/otlplog/otlploggrpc/internal/x/READEME.md new file mode 100644 index 00000000000..1cb38eb8390 --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/READEME.md @@ -0,0 +1,36 @@ +# Experimental Features + +The metric SDK contains features that have not yet stabilized in the OpenTelemetry specification. +These features are added to the OpenTelemetry Go metric SDK prior to stabilization in the specification so that users can start experimenting with them and provide feedback. + +These feature may change in backwards incompatible ways as feedback is applied. +See the [Compatibility and Stability](#compatibility-and-stability) section for more information. + +## Features + +- [Self-Observability](#self-observability) + +### Self-Observability + +The SDK provides a self-observability feature that allows you to monitor the SDK itself. + +To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`. + +When enabled, the SDK will create the following metrics using the global `MeterProvider`: + +- `otel.sdk.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. From 2e1da4ae8ccd72dc55e243da84604761ffe28656 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Jul 2025 21:23:10 +0800 Subject: [PATCH 09/42] fix spell --- .../otlp/otlplog/otlploggrpc/internal/x/{READEME.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename exporters/otlp/otlplog/otlploggrpc/internal/x/{READEME.md => README.md} (100%) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/READEME.md b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md similarity index 100% rename from exporters/otlp/otlplog/otlploggrpc/internal/x/READEME.md rename to exporters/otlp/otlplog/otlploggrpc/internal/x/README.md From d228db82dc2185c25b388852c7a07f5b6d6581cf Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 28 Jul 2025 23:53:02 +0800 Subject: [PATCH 10/42] add the error tests --- exporters/otlp/otlplog/otlploggrpc/client.go | 9 +- .../otlp/otlplog/otlploggrpc/client_test.go | 108 +++++++++++++++++- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index dc5fcb6e14a..8746495af89 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -34,10 +34,7 @@ import ( logpb "go.opentelemetry.io/proto/otlp/logs/v1" ) -var ( - componentType = otelconv.ComponentTypeOtlpGRPCLogExporter - exporterInstanceCounter atomic.Int64 -) +var grpcExporterCounter atomic.Int64 // The methods of this type are not expected to be called concurrently. type client struct { @@ -101,10 +98,10 @@ func (c *client) initSelfObservability() { return } - counter := exporterInstanceCounter.Add(1) + counter := grpcExporterCounter.Add(1) c.selfObservabilityEnabled = true c.port = c.getPort() - c.componentName = fmt.Sprintf("%s/%d", componentType, counter) + c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, counter) mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 1d78b72beb6..cc4489ef3d4 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -764,9 +764,7 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName( - fmt.Sprintf("%s/%d", componentType, 2), - ), + otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), @@ -828,7 +826,109 @@ func TestSelfObservability(t *testing.T) { g := scopeMetrics() normalizeMetrics(&g) metricdatatest.AssertEqual(t, wantMetrics, g, metricdatatest.IgnoreTimestamp()) - exporterInstanceCounter.Add(-1) + }, + }, + { + name: "upload failure", + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + wantErr := status.Error(codes.InvalidArgument, "request contains invalid arguments") + wantErrTypeAttr := semconv.ErrorType(wantErr) + wantGRPCStatusCodeAttr := otelconv.RPCGRPCStatusCodeAttr(codes.InvalidArgument) + rCh := make(chan exportResult, 1) + rCh <- exportResult{ + Err: wantErr, + } + ctx := context.Background() + client, _ := clientFactory(t, rCh) + err := client.UploadLogs(ctx, resourceLogs) + assert.ErrorContains(t, err, "request contains invalid arguments") + + wantMetrics := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKExporterLogInflight{}.Name(), + Description: otelconv.SDKExporterLogInflight{}.Description(), + Unit: otelconv.SDKExporterLogInflight{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogInflight{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogInflight{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), + otelconv.SDKExporterLogInflight{}.AttrServerPort(client.port), + wantErrTypeAttr, + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKExporterLogExported{}.Name(), + Description: otelconv.SDKExporterLogExported{}.Description(), + Unit: otelconv.SDKExporterLogExported{}.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), + otelconv.SDKExporterLogExported{}.AttrServerPort(client.port), + wantErrTypeAttr, + ), + Value: 1, + }, + }, + }, + }, + { + Name: otelconv.SDKExporterOperationDuration{}.Name(), + Description: otelconv.SDKExporterOperationDuration{}.Description(), + Unit: otelconv.SDKExporterOperationDuration{}.Unit(), + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterOperationDuration{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + otelconv.SDKExporterOperationDuration{}.AttrServerAddress( + client.conn.Target(), + ), + otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.port), + otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( + wantGRPCStatusCodeAttr, + ), + otelconv.SDKExporterOperationDuration{}.AttrErrorType( + otelconv.ErrorTypeAttr(wantErr.Error()), + ), + ), + Count: 1, + }, + }, + }, + }, + }, + } + g := scopeMetrics() + normalizeMetrics(&g) + metricdatatest.AssertEqual(t, wantMetrics, g, metricdatatest.IgnoreTimestamp()) }, }, } From 7a418315846bb23505cc168237c5fae493ebdd9d Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 29 Jul 2025 00:07:36 +0800 Subject: [PATCH 11/42] improve error handling --- exporters/otlp/otlplog/otlploggrpc/client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 8746495af89..307db3e946b 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -290,11 +290,13 @@ func (c *client) recordLogExportedDurationMetric( func (c *client) getPort() int { _, p, err := net.SplitHostPort(c.conn.Target()) if err != nil { + err = fmt.Errorf("OTLP getPort: failed to split host and port from target '%s': %w", c.conn.Target(), err) otel.Handle(err) } port, err := strconv.Atoi(p) if err != nil { + err = fmt.Errorf("OTLP getPort: failed to convert port string '%s' to integer: %w", p, err) otel.Handle(err) } return port From 6797188827d641128899a15ee40546654ea7409e Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 29 Jul 2025 18:43:50 +0800 Subject: [PATCH 12/42] refactor --- exporters/otlp/otlplog/otlploggrpc/client.go | 31 ++++++------- .../otlp/otlplog/otlploggrpc/client_test.go | 44 ++++++++----------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 307db3e946b..8ba2d2b7592 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -50,7 +50,7 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient - port int + addressAttrs []attribute.KeyValue componentName string selfObservabilityEnabled bool logInflightMetric otelconv.SDKExporterLogInflight @@ -100,7 +100,7 @@ func (c *client) initSelfObservability() { counter := grpcExporterCounter.Add(1) c.selfObservabilityEnabled = true - c.port = c.getPort() + c.addressAttrs = c.serverAddrAttrs() c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, counter) mp := otel.GetMeterProvider() @@ -241,12 +241,10 @@ func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attr attrs := []attribute.KeyValue{ c.logInflightMetric.AttrComponentName(c.componentName), c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - c.logInflightMetric.AttrServerAddress(c.conn.Target()), - c.logInflightMetric.AttrServerPort(c.port), } attrs = append(attrs, extraAttrs...) - + attrs = append(attrs, c.addressAttrs...) c.logInflightMetric.Add(ctx, 1, attrs...) } @@ -258,11 +256,10 @@ func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attr attrs := []attribute.KeyValue{ c.logExportedMetric.AttrComponentName(c.componentName), c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - c.logExportedMetric.AttrServerAddress(c.conn.Target()), - c.logExportedMetric.AttrServerPort(c.port), } attrs = append(attrs, extraAttrs...) + attrs = append(attrs, c.addressAttrs...) c.logExportedMetric.Add(ctx, 1, attrs...) } @@ -278,28 +275,28 @@ func (c *client) recordLogExportedDurationMetric( attrs := []attribute.KeyValue{ c.logExportedDurationMetric.AttrComponentName(c.componentName), c.logExportedDurationMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - c.logExportedDurationMetric.AttrServerAddress(c.conn.Target()), - c.logExportedMetric.AttrServerPort(c.port), } attrs = append(attrs, extraAttrs...) + attrs = append(attrs, c.addressAttrs...) c.logExportedDurationMetric.Record(ctx, duration, attrs...) } -func (c *client) getPort() int { - _, p, err := net.SplitHostPort(c.conn.Target()) +func (c *client) serverAddrAttrs() []attribute.KeyValue { + host, pStr, err := net.SplitHostPort(c.conn.Target()) if err != nil { - err = fmt.Errorf("OTLP getPort: failed to split host and port from target '%s': %w", c.conn.Target(), err) - otel.Handle(err) + return []attribute.KeyValue{semconv.ServerAddress(c.conn.Target())} } - port, err := strconv.Atoi(p) + port, err := strconv.Atoi(pStr) if err != nil { - err = fmt.Errorf("OTLP getPort: failed to convert port string '%s' to integer: %w", p, err) - otel.Handle(err) + return []attribute.KeyValue{semconv.ServerAddress(host)} + } + return []attribute.KeyValue{ + semconv.ServerAddress(host), + semconv.ServerPort(port), } - return port } // Shutdown shuts down the client, freeing all resources. diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index cc4489ef3d4..77933b9e53b 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -640,8 +640,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogInflight{}.AttrServerPort(client.port), + client.addressAttrs[0], + client.addressAttrs[1], ), Value: 1, }, @@ -662,8 +662,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogExported{}.AttrServerPort(client.port), + client.addressAttrs[0], + client.addressAttrs[1], ), Value: 1, }, @@ -683,10 +683,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterOperationDuration{}.AttrServerAddress( - client.conn.Target(), - ), - otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.port), + client.addressAttrs[0], + client.addressAttrs[1], ), Count: 1, }, @@ -709,7 +707,7 @@ func TestSelfObservability(t *testing.T) { }, }, { - name: "PartialSuccess", + name: "partial success", test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { const n, msg = 2, "bad data" rCh := make(chan exportResult, 1) @@ -745,9 +743,9 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogInflight{}.AttrServerPort(client.port), semconv.ErrorType(wantErr), + client.addressAttrs[0], + client.addressAttrs[1], ), Value: 1, }, @@ -768,9 +766,9 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogExported{}.AttrServerPort(client.port), semconv.ErrorType(wantErr), + client.addressAttrs[0], + client.addressAttrs[1], ), Value: 1, }, @@ -790,16 +788,14 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterOperationDuration{}.AttrServerAddress( - client.conn.Target(), - ), - otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.port), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr( status.Code(wantErr), ), ), semconv.ErrorType(wantErr), + client.addressAttrs[0], + client.addressAttrs[1], ), Count: 1, }, @@ -863,8 +859,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterLogInflight{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogInflight{}.AttrServerPort(client.port), + client.addressAttrs[0], + client.addressAttrs[1], wantErrTypeAttr, ), Value: 1, @@ -886,8 +882,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterLogExported{}.AttrServerAddress(client.conn.Target()), - otelconv.SDKExporterLogExported{}.AttrServerPort(client.port), + client.addressAttrs[0], + client.addressAttrs[1], wantErrTypeAttr, ), Value: 1, @@ -908,16 +904,14 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - otelconv.SDKExporterOperationDuration{}.AttrServerAddress( - client.conn.Target(), - ), - otelconv.SDKExporterOperationDuration{}.AttrServerPort(client.port), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( wantGRPCStatusCodeAttr, ), otelconv.SDKExporterOperationDuration{}.AttrErrorType( otelconv.ErrorTypeAttr(wantErr.Error()), ), + client.addressAttrs[0], + client.addressAttrs[1], ), Count: 1, }, From caea157afa8208a6acede0c10e7ff08dfbe4739e Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 29 Jul 2025 19:39:51 +0800 Subject: [PATCH 13/42] rename filed --- exporters/otlp/otlplog/otlploggrpc/client.go | 10 +++--- .../otlp/otlplog/otlploggrpc/client_test.go | 36 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 8ba2d2b7592..a9bf041079a 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -50,7 +50,7 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient - addressAttrs []attribute.KeyValue + presetAttrs []attribute.KeyValue componentName string selfObservabilityEnabled bool logInflightMetric otelconv.SDKExporterLogInflight @@ -100,7 +100,7 @@ func (c *client) initSelfObservability() { counter := grpcExporterCounter.Add(1) c.selfObservabilityEnabled = true - c.addressAttrs = c.serverAddrAttrs() + c.presetAttrs = c.serverAddrAttrs() c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, counter) mp := otel.GetMeterProvider() @@ -244,7 +244,7 @@ func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attr } attrs = append(attrs, extraAttrs...) - attrs = append(attrs, c.addressAttrs...) + attrs = append(attrs, c.presetAttrs...) c.logInflightMetric.Add(ctx, 1, attrs...) } @@ -259,7 +259,7 @@ func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attr } attrs = append(attrs, extraAttrs...) - attrs = append(attrs, c.addressAttrs...) + attrs = append(attrs, c.presetAttrs...) c.logExportedMetric.Add(ctx, 1, attrs...) } @@ -278,7 +278,7 @@ func (c *client) recordLogExportedDurationMetric( } attrs = append(attrs, extraAttrs...) - attrs = append(attrs, c.addressAttrs...) + attrs = append(attrs, c.presetAttrs...) c.logExportedDurationMetric.Record(ctx, duration, attrs...) } diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 77933b9e53b..4fe506b05f7 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -640,8 +640,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Value: 1, }, @@ -662,8 +662,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Value: 1, }, @@ -683,8 +683,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Count: 1, }, @@ -744,8 +744,8 @@ func TestSelfObservability(t *testing.T) { otelconv.ComponentTypeOtlpGRPCLogExporter, ), semconv.ErrorType(wantErr), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Value: 1, }, @@ -767,8 +767,8 @@ func TestSelfObservability(t *testing.T) { otelconv.ComponentTypeOtlpGRPCLogExporter, ), semconv.ErrorType(wantErr), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Value: 1, }, @@ -794,8 +794,8 @@ func TestSelfObservability(t *testing.T) { ), ), semconv.ErrorType(wantErr), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Count: 1, }, @@ -859,8 +859,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], wantErrTypeAttr, ), Value: 1, @@ -882,8 +882,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], wantErrTypeAttr, ), Value: 1, @@ -910,8 +910,8 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterOperationDuration{}.AttrErrorType( otelconv.ErrorTypeAttr(wantErr.Error()), ), - client.addressAttrs[0], - client.addressAttrs[1], + client.presetAttrs[0], + client.presetAttrs[1], ), Count: 1, }, From 35f92917a703eddcc39681f77d5f27ccac843700 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 30 Jul 2025 22:38:14 +0800 Subject: [PATCH 14/42] fix some errors in the document, renamed the grpcExporterCounter and fix the counter --- CHANGELOG.md | 2 +- exporters/otlp/otlplog/otlploggrpc/client.go | 8 +++++--- .../otlp/otlplog/otlploggrpc/internal/x/README.md | 10 +++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4f4f88449..02ea32308a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032) - Add experimental self-observability span metrics in `go.opentelemetry.io/otel/sdk/trace`. Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027) -- Add experimental self-observability metrics `exporter.log.inflight`, `exporter.log.exported`, and `exporter.operation.duration` in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. +- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. Check the `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x` package documentation for more information. (#7084) ### Changed diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index a9bf041079a..dd8493a8122 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -34,7 +34,7 @@ import ( logpb "go.opentelemetry.io/proto/otlp/logs/v1" ) -var grpcExporterCounter atomic.Int64 +var grpcExporterIDCounter atomic.Int64 // The methods of this type are not expected to be called concurrently. type client struct { @@ -98,10 +98,12 @@ func (c *client) initSelfObservability() { return } - counter := grpcExporterCounter.Add(1) + id := grpcExporterIDCounter.Load() + grpcExporterIDCounter.Add(1) + c.selfObservabilityEnabled = true c.presetAttrs = c.serverAddrAttrs() - c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, counter) + c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md index 1cb38eb8390..f80a4cf7f6e 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md @@ -1,7 +1,7 @@ # Experimental Features -The metric SDK contains features that have not yet stabilized in the OpenTelemetry specification. -These features are added to the OpenTelemetry Go metric SDK prior to stabilization in the specification so that users can start experimenting with them and provide feedback. +The `otlploggrpc` exporter contains features that have not yet stabilized in the OpenTelemetry specification. +These features are added to the `otlploggrpc` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback. These feature may change in backwards incompatible ways as feedback is applied. See the [Compatibility and Stability](#compatibility-and-stability) section for more information. @@ -12,11 +12,11 @@ See the [Compatibility and Stability](#compatibility-and-stability) section for ### Self-Observability -The SDK provides a self-observability feature that allows you to monitor the SDK itself. +The `otlploggrpc` exporter provides a self-observability feature that allows you to monitor the exporter itself. To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`. -When enabled, the SDK will create the following metrics using the global `MeterProvider`: +When enabled, the exporter will create the following metrics using the global `MeterProvider`: - `otel.sdk.exporter.log.inflight` - `otel.sdk.exporter.log.exported` @@ -28,7 +28,7 @@ Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentatio ## Compatibility and Stability -Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md). +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. From 4cbf661342ce4f45ba690b8f9e58db42cad30008 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 30 Jul 2025 23:00:27 +0800 Subject: [PATCH 15/42] remove space --- exporters/otlp/otlplog/otlploggrpc/internal/x/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md index f80a4cf7f6e..326e6bc5b80 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md @@ -1,7 +1,7 @@ # Experimental Features The `otlploggrpc` exporter contains features that have not yet stabilized in the OpenTelemetry specification. -These features are added to the `otlploggrpc` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback. +These features are added to the `otlploggrpc` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback. These feature may change in backwards incompatible ways as feedback is applied. See the [Compatibility and Stability](#compatibility-and-stability) section for more information. @@ -12,7 +12,7 @@ See the [Compatibility and Stability](#compatibility-and-stability) section for ### Self-Observability -The `otlploggrpc` exporter provides a self-observability feature that allows you to monitor the exporter itself. +The `otlploggrpc` exporter provides a self-observability feature that allows you to monitor the exporter itself. To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`. From 966afedbf74d9d945fe2afa4c061951848fdb2b4 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 30 Jul 2025 23:06:42 +0800 Subject: [PATCH 16/42] use equalFlod --- exporters/otlp/otlplog/otlploggrpc/internal/x/x.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go b/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go index 24251f82f98..b609f2deb28 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/x.go @@ -34,7 +34,7 @@ func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { // to the case-insensitive string value of "true" (i.e. "True" and "TRUE" // will also enable this). var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) { - if strings.ToLower(v) == "true" { + if strings.EqualFold(v, "true") { return v, true } return "", false From 49531d2b7b2148df2c5ab84ee981370ee4c970c1 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 30 Jul 2025 23:20:03 +0800 Subject: [PATCH 17/42] fix data race of counter --- exporters/otlp/otlplog/otlploggrpc/client.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 81d49486e1e..9daaabaa4f1 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -98,12 +98,11 @@ func (c *client) initSelfObservability() { return } - id := grpcExporterIDCounter.Load() - grpcExporterIDCounter.Add(1) + id := grpcExporterIDCounter.Add(1) c.selfObservabilityEnabled = true c.presetAttrs = c.serverAddrAttrs() - c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) + c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id-1) mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", From e0bb5f780a97fd5273ee0fd4efae93f01b699c76 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 30 Jul 2025 23:30:52 +0800 Subject: [PATCH 18/42] updated --- exporters/otlp/otlplog/otlploggrpc/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 9daaabaa4f1..9f6a9beb785 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -98,11 +98,11 @@ func (c *client) initSelfObservability() { return } - id := grpcExporterIDCounter.Add(1) + id := grpcExporterIDCounter.Add(1) - 1 c.selfObservabilityEnabled = true c.presetAttrs = c.serverAddrAttrs() - c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id-1) + c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) mp := otel.GetMeterProvider() m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", From 57ec77911eaaa15033123772be98d5a6f5ea7cf0 Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 31 Jul 2025 18:24:19 +0800 Subject: [PATCH 19/42] refactor: pre-allocate --- exporters/otlp/otlplog/otlploggrpc/client.go | 31 +++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 9f6a9beb785..de31a6e9b9e 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -240,11 +240,14 @@ func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attr if !c.selfObservabilityEnabled { return } - attrs := []attribute.KeyValue{ - c.logInflightMetric.AttrComponentName(c.componentName), - c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - } + totalCap := len(extraAttrs) + len(c.presetAttrs) + 2 + attrs := make([]attribute.KeyValue, 0, totalCap) + + attrs = append( + attrs, + c.logInflightMetric.AttrComponentName(c.componentName), + c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter)) attrs = append(attrs, extraAttrs...) attrs = append(attrs, c.presetAttrs...) c.logInflightMetric.Add(ctx, 1, attrs...) @@ -255,11 +258,14 @@ func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attr return } - attrs := []attribute.KeyValue{ + totalCap := len(extraAttrs) + len(c.presetAttrs) + 2 + attrs := make([]attribute.KeyValue, 0, totalCap) + + attrs = append( + attrs, c.logExportedMetric.AttrComponentName(c.componentName), c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - } - + ) attrs = append(attrs, extraAttrs...) attrs = append(attrs, c.presetAttrs...) c.logExportedMetric.Add(ctx, 1, attrs...) @@ -274,10 +280,13 @@ func (c *client) recordLogExportedDurationMetric( return } - attrs := []attribute.KeyValue{ - c.logExportedDurationMetric.AttrComponentName(c.componentName), - c.logExportedDurationMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - } + totalCap := len(extraAttrs) + len(c.presetAttrs) + 2 + attrs := make([]attribute.KeyValue, 0, totalCap) + attrs = append( + attrs, + c.logExportedMetric.AttrComponentName(c.componentName), + c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), + ) attrs = append(attrs, extraAttrs...) attrs = append(attrs, c.presetAttrs...) From 95008b1ab61d8fb68989572bf18b72bd3478af79 Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 31 Jul 2025 22:50:29 +0800 Subject: [PATCH 20/42] feat: Use defer to record the duration of Export calls --- exporters/otlp/otlplog/otlploggrpc/client.go | 34 ++++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index de31a6e9b9e..37e83f35b3f 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -50,6 +50,7 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient + duration time.Duration presetAttrs []attribute.KeyValue componentName string selfObservabilityEnabled bool @@ -180,20 +181,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error defer cancel() return c.requestFunc(ctx, func(ctx context.Context) error { - var ( - begin time.Time - duration time.Duration - ) - if c.selfObservabilityEnabled { - begin = time.Now() - } - resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ - ResourceLogs: rl, - }) - - if c.selfObservabilityEnabled { - duration = time.Since(begin) - } + resp, err := c.export(ctx, rl) if resp != nil && resp.PartialSuccess != nil { msg := resp.PartialSuccess.GetErrorMessage() @@ -204,7 +192,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error c.recordLogExportedMetric(context.Background(), semconv.ErrorType(err)) c.recordLogExportedDurationMetric( context.Background(), - duration.Seconds(), + c.duration.Seconds(), c.logExportedDurationMetric.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), ), @@ -219,14 +207,14 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error // Success. c.recordLogInflightMetric(context.Background()) c.recordLogExportedMetric(context.Background()) - c.recordLogExportedDurationMetric(context.Background(), duration.Seconds()) + c.recordLogExportedDurationMetric(context.Background(), c.duration.Seconds()) return nil } c.recordLogInflightMetric(context.Background(), semconv.ErrorType(err)) c.recordLogExportedMetric(context.Background(), semconv.ErrorType(err)) c.recordLogExportedDurationMetric( context.Background(), - duration.Seconds(), + c.duration.Seconds(), c.logExportedDurationMetric.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), ), @@ -236,6 +224,18 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error }) } +func (c *client) export(ctx context.Context, rl []*logpb.ResourceLogs) (*collogpb.ExportLogsServiceResponse, error) { + if c.selfObservabilityEnabled { + begin := time.Now() + defer func() { + c.duration = time.Since(begin) + }() + } + return c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ + ResourceLogs: rl, + }) +} + func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attribute.KeyValue) { if !c.selfObservabilityEnabled { return From 9b63d1ac8f4229d6671340855030f6df47e2fcb5 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 4 Aug 2025 22:16:06 +0800 Subject: [PATCH 21/42] correctly identify and strip the scheme (e.g., `dns://`, `unix://`). --- exporters/otlp/otlplog/otlploggrpc/client.go | 21 ++++++--- .../otlp/otlplog/otlploggrpc/client_test.go | 43 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 0882498f9a8..c336892189d 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "strconv" + "strings" "sync/atomic" "time" @@ -102,7 +103,7 @@ func (c *client) initSelfObservability() { id := grpcExporterIDCounter.Add(1) - 1 c.selfObservabilityEnabled = true - c.presetAttrs = c.serverAddrAttrs() + c.presetAttrs = serverAddrAttrs(c.conn.Target()) c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) mp := otel.GetMeterProvider() @@ -247,7 +248,8 @@ func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attr attrs = append( attrs, c.logInflightMetric.AttrComponentName(c.componentName), - c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter)) + c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), + ) attrs = append(attrs, extraAttrs...) attrs = append(attrs, c.presetAttrs...) c.logInflightMetric.Add(ctx, 1, attrs...) @@ -294,10 +296,19 @@ func (c *client) recordLogExportedDurationMetric( c.logExportedDurationMetric.Record(ctx, duration, attrs...) } -func (c *client) serverAddrAttrs() []attribute.KeyValue { - host, pStr, err := net.SplitHostPort(c.conn.Target()) +func serverAddrAttrs(target string) []attribute.KeyValue { + if strings.HasPrefix(target, "unix://") { + path := strings.TrimPrefix(target, "unix://") + return []attribute.KeyValue{semconv.ServerAddress(path)} + } + + if idx := strings.Index(target, "://"); idx != -1 { + target = target[idx+4:] + } + + host, pStr, err := net.SplitHostPort(target) if err != nil { - return []attribute.KeyValue{semconv.ServerAddress(c.conn.Target())} + return []attribute.KeyValue{semconv.ServerAddress(target)} } port, err := strconv.Atoi(pStr) diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index eef3004cacf..b6bde6839d3 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -38,15 +38,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/log" -<<<<<<< HEAD semconv "go.opentelemetry.io/otel/semconv/v1.36.0" - collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" - cpb "go.opentelemetry.io/proto/otlp/common/v1" - lpb "go.opentelemetry.io/proto/otlp/logs/v1" - rpb "go.opentelemetry.io/proto/otlp/resource/v1" -======= - semconv "go.opentelemetry.io/otel/semconv/v1.34.0" ->>>>>>> upstream/main ) var ( @@ -977,3 +969,38 @@ func normalizeMetrics(scopeMetrics *metricdata.ScopeMetrics) { } } } + +func TestServerAddrAttrs(t *testing.T) { + testcases := []struct { + name string + target string + want []attribute.KeyValue + }{ + { + name: "UnixSocket", + target: "unix:///tmp/grpc.sock", + want: []attribute.KeyValue{semconv.ServerAddress("/tmp/grpc.sock")}, + }, + { + name: "DNSWithPort", + target: "dns:///localhost:8080", + want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(8080)}, + }, + { + name: "SimpleHostPort", + target: "localhost:10001", + want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(10001)}, + }, + { + name: "HostWithoutPort", + target: "example.com", + want: []attribute.KeyValue{semconv.ServerAddress("example.com")}, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + attrs := serverAddrAttrs(tc.target) + assert.Equal(t, tc.want, attrs) + }) + } +} From 6abf09902a398bc3451b5b3b0bcdbd180bd50fe2 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 5 Aug 2025 21:15:09 +0800 Subject: [PATCH 22/42] refactor(otlploggrpc): Extract all exporter metrics recording code from `client.go` into selfobservability.go. --- exporters/otlp/otlplog/otlploggrpc/client.go | 172 +++--------------- .../otlp/otlplog/otlploggrpc/client_test.go | 136 +++++++------- .../selfobservability/selfobservability.go | 104 +++++++++++ .../selfobservability_test.go | 49 +++++ 4 files changed, 236 insertions(+), 225 deletions(-) create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index c336892189d..5f6c665b4bd 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -7,12 +7,11 @@ import ( "context" "errors" "fmt" - "net" - "strconv" - "strings" "sync/atomic" "time" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" + collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" logpb "go.opentelemetry.io/proto/otlp/logs/v1" "google.golang.org/genproto/googleapis/rpc/errdetails" @@ -26,12 +25,8 @@ import ( "google.golang.org/grpc/status" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/sdk" - semconv "go.opentelemetry.io/otel/semconv/v1.36.0" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) @@ -51,13 +46,8 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient - duration time.Duration - presetAttrs []attribute.KeyValue - componentName string - selfObservabilityEnabled bool - logInflightMetric otelconv.SDKExporterLogInflight - logExportedMetric otelconv.SDKExporterLogExported - logExportedDurationMetric otelconv.SDKExporterOperationDuration + selfObservabilityEnabled bool + exporterMetric *selfobservability.ExporterMetrics } // Used for testing. @@ -100,27 +90,13 @@ func (c *client) initSelfObservability() { return } - id := grpcExporterIDCounter.Add(1) - 1 - c.selfObservabilityEnabled = true - c.presetAttrs = serverAddrAttrs(c.conn.Target()) - c.componentName = fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) - - mp := otel.GetMeterProvider() - m := mp.Meter("go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", - metric.WithInstrumentationVersion(sdk.Version()), - metric.WithSchemaURL(semconv.SchemaURL)) - - var err error - if c.logInflightMetric, err = otelconv.NewSDKExporterLogInflight(m); err != nil { - otel.Handle(err) - } - if c.logExportedMetric, err = otelconv.NewSDKExporterLogExported(m); err != nil { - otel.Handle(err) - } - if c.logExportedDurationMetric, err = otelconv.NewSDKExporterOperationDuration(m); err != nil { - otel.Handle(err) - } + id := grpcExporterIDCounter.Add(1) - 1 + componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) + c.exporterMetric = selfobservability.NewExporterMetrics( + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", + componentName, + string(otelconv.ComponentTypeOtlpGRPCLogExporter), c.conn.Target()) } func newGRPCDialOptions(cfg config) []grpc.DialOption { @@ -173,7 +149,6 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error select { case <-ctx.Done(): // Do not upload if the context is already expired. - c.recordLogInflightMetric(context.Background(), semconv.ErrorType(ctx.Err())) return ctx.Err() default: } @@ -182,23 +157,18 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error defer cancel() return c.requestFunc(ctx, func(ctx context.Context) error { - resp, err := c.export(ctx, rl) + trackExportFunc := c.trackExport(context.Background()) + + resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ + ResourceLogs: rl, + }) if resp != nil && resp.PartialSuccess != nil { msg := resp.PartialSuccess.GetErrorMessage() n := resp.PartialSuccess.GetRejectedLogRecords() if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) - c.recordLogInflightMetric(context.Background(), semconv.ErrorType(err)) - c.recordLogExportedMetric(context.Background(), semconv.ErrorType(err)) - c.recordLogExportedDurationMetric( - context.Background(), - c.duration.Seconds(), - c.logExportedDurationMetric.AttrRPCGRPCStatusCode( - otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), - ), - semconv.ErrorType(err), - ) + trackExportFunc(err, int(status.Code(err))) otel.Handle(err) return nil } @@ -206,119 +176,19 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error // nil is converted to OK. if status.Code(err) == codes.OK { // Success. - c.recordLogInflightMetric(context.Background()) - c.recordLogExportedMetric(context.Background()) - c.recordLogExportedDurationMetric(context.Background(), c.duration.Seconds()) + trackExportFunc(nil, int(codes.OK)) return nil } - c.recordLogInflightMetric(context.Background(), semconv.ErrorType(err)) - c.recordLogExportedMetric(context.Background(), semconv.ErrorType(err)) - c.recordLogExportedDurationMetric( - context.Background(), - c.duration.Seconds(), - c.logExportedDurationMetric.AttrRPCGRPCStatusCode( - otelconv.RPCGRPCStatusCodeAttr(status.Code(err)), - ), - c.logExportedDurationMetric.AttrErrorType(otelconv.ErrorTypeAttr(err.Error())), - ) + trackExportFunc(err, int(status.Code(err))) return err }) } -func (c *client) export(ctx context.Context, rl []*logpb.ResourceLogs) (*collogpb.ExportLogsServiceResponse, error) { - if c.selfObservabilityEnabled { - begin := time.Now() - defer func() { - c.duration = time.Since(begin) - }() - } - return c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ - ResourceLogs: rl, - }) -} - -func (c *client) recordLogInflightMetric(ctx context.Context, extraAttrs ...attribute.KeyValue) { +func (c *client) trackExport(ctx context.Context) func(err error, code int) { if !c.selfObservabilityEnabled { - return - } - - totalCap := len(extraAttrs) + len(c.presetAttrs) + 2 - attrs := make([]attribute.KeyValue, 0, totalCap) - - attrs = append( - attrs, - c.logInflightMetric.AttrComponentName(c.componentName), - c.logInflightMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - ) - attrs = append(attrs, extraAttrs...) - attrs = append(attrs, c.presetAttrs...) - c.logInflightMetric.Add(ctx, 1, attrs...) -} - -func (c *client) recordLogExportedMetric(ctx context.Context, extraAttrs ...attribute.KeyValue) { - if !c.selfObservabilityEnabled { - return - } - - totalCap := len(extraAttrs) + len(c.presetAttrs) + 2 - attrs := make([]attribute.KeyValue, 0, totalCap) - - attrs = append( - attrs, - c.logExportedMetric.AttrComponentName(c.componentName), - c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - ) - attrs = append(attrs, extraAttrs...) - attrs = append(attrs, c.presetAttrs...) - c.logExportedMetric.Add(ctx, 1, attrs...) -} - -func (c *client) recordLogExportedDurationMetric( - ctx context.Context, - duration float64, - extraAttrs ...attribute.KeyValue, -) { - if !c.selfObservabilityEnabled { - return - } - - totalCap := len(extraAttrs) + len(c.presetAttrs) + 2 - attrs := make([]attribute.KeyValue, 0, totalCap) - attrs = append( - attrs, - c.logExportedMetric.AttrComponentName(c.componentName), - c.logExportedMetric.AttrComponentType(otelconv.ComponentTypeOtlpGRPCLogExporter), - ) - - attrs = append(attrs, extraAttrs...) - attrs = append(attrs, c.presetAttrs...) - - c.logExportedDurationMetric.Record(ctx, duration, attrs...) -} - -func serverAddrAttrs(target string) []attribute.KeyValue { - if strings.HasPrefix(target, "unix://") { - path := strings.TrimPrefix(target, "unix://") - return []attribute.KeyValue{semconv.ServerAddress(path)} - } - - if idx := strings.Index(target, "://"); idx != -1 { - target = target[idx+4:] - } - - host, pStr, err := net.SplitHostPort(target) - if err != nil { - return []attribute.KeyValue{semconv.ServerAddress(target)} - } - - port, err := strconv.Atoi(pStr) - if err != nil { - return []attribute.KeyValue{semconv.ServerAddress(host)} - } - return []attribute.KeyValue{ - semconv.ServerAddress(host), - semconv.ServerPort(port), + return func(_ error, _ int) {} } + return c.exporterMetric.TrackExport(ctx) } // Shutdown shuts down the client, freeing all resources. diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index b6bde6839d3..097a7c46652 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -11,6 +11,12 @@ import ( "testing" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,12 +34,8 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/sdk" - "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "go.opentelemetry.io/otel" @@ -620,6 +622,12 @@ func TestSelfObservability(t *testing.T) { test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { ctx := context.Background() client, coll := clientFactory(t, nil) + componentName := fmt.Sprintf( + "%s/%d", + otelconv.ComponentTypeOtlpGRPCLogExporter, + grpcExporterIDCounter.Load()-1, + ) + serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) want := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -636,14 +644,14 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogInflight{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogInflight{}.AttrComponentName(componentName), otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], ), - Value: 1, + Value: 0, }, }, }, @@ -658,12 +666,12 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], ), Value: 1, }, @@ -679,12 +687,17 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.presetAttrs[0], - client.presetAttrs[1], + otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( + otelconv.RPCGRPCStatusCodeAttr( + codes.OK, + ), + ), + serverAddrAttrs[0], + serverAddrAttrs[1], ), Count: 1, }, @@ -693,6 +706,7 @@ func TestSelfObservability(t *testing.T) { }, }, } + require.NoError(t, client.UploadLogs(ctx, resourceLogs)) require.NoError(t, client.Shutdown(ctx)) got := coll.Collect().Dump() @@ -722,7 +736,13 @@ func TestSelfObservability(t *testing.T) { ctx := context.Background() client, _ := clientFactory(t, rCh) + componentName := fmt.Sprintf( + "%s/%d", + otelconv.ComponentTypeOtlpGRPCLogExporter, + grpcExporterIDCounter.Load()-1, + ) wantErr := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) + serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -739,15 +759,15 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogInflight{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogInflight{}.AttrComponentName(componentName), otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - semconv.ErrorType(wantErr), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], ), - Value: 1, + + Value: 0, }, }, }, @@ -762,13 +782,13 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), semconv.ErrorType(wantErr), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], ), Value: 1, }, @@ -784,7 +804,7 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), @@ -794,8 +814,8 @@ func TestSelfObservability(t *testing.T) { ), ), semconv.ErrorType(wantErr), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], ), Count: 1, }, @@ -839,6 +859,12 @@ func TestSelfObservability(t *testing.T) { err := client.UploadLogs(ctx, resourceLogs) assert.ErrorContains(t, err, "request contains invalid arguments") + componentName := fmt.Sprintf( + "%s/%d", + otelconv.ComponentTypeOtlpGRPCLogExporter, + grpcExporterIDCounter.Load()-1, + ) + serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -855,15 +881,14 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogInflight{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogInflight{}.AttrComponentName(componentName), otelconv.SDKExporterLogInflight{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.presetAttrs[0], - client.presetAttrs[1], - wantErrTypeAttr, + serverAddrAttrs[0], + serverAddrAttrs[1], ), - Value: 1, + Value: 0, }, }, }, @@ -878,12 +903,12 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.DataPoint[int64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], wantErrTypeAttr, ), Value: 1, @@ -900,18 +925,16 @@ func TestSelfObservability(t *testing.T) { DataPoints: []metricdata.HistogramDataPoint[float64]{ { Attributes: attribute.NewSet( - otelconv.SDKExporterLogExported{}.AttrComponentName(client.componentName), + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), otelconv.SDKExporterOperationDuration{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( wantGRPCStatusCodeAttr, ), - otelconv.SDKExporterOperationDuration{}.AttrErrorType( - otelconv.ErrorTypeAttr(wantErr.Error()), - ), - client.presetAttrs[0], - client.presetAttrs[1], + serverAddrAttrs[0], + serverAddrAttrs[1], + wantErrTypeAttr, ), Count: 1, }, @@ -969,38 +992,3 @@ func normalizeMetrics(scopeMetrics *metricdata.ScopeMetrics) { } } } - -func TestServerAddrAttrs(t *testing.T) { - testcases := []struct { - name string - target string - want []attribute.KeyValue - }{ - { - name: "UnixSocket", - target: "unix:///tmp/grpc.sock", - want: []attribute.KeyValue{semconv.ServerAddress("/tmp/grpc.sock")}, - }, - { - name: "DNSWithPort", - target: "dns:///localhost:8080", - want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(8080)}, - }, - { - name: "SimpleHostPort", - target: "localhost:10001", - want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(10001)}, - }, - { - name: "HostWithoutPort", - target: "example.com", - want: []attribute.KeyValue{semconv.ServerAddress("example.com")}, - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - attrs := serverAddrAttrs(tc.target) - assert.Equal(t, tc.want, attrs) - }) - } -} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go new file mode 100644 index 00000000000..5f902771afa --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package selfobservability provides self-observability metrics for OTLP log exporters. +// This is an experimental feature controlled by the x.SelfObservability feature flag. +package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" + +import ( + "context" + "net" + "strconv" + "strings" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" +) + +type ExporterMetrics struct { + selfObservabilityEnabled bool + logInflightMetric otelconv.SDKExporterLogInflight + logExportedMetric otelconv.SDKExporterLogExported + logExportedDurationMetric otelconv.SDKExporterOperationDuration + presetAttrs []attribute.KeyValue +} + +func NewExporterMetrics(name, componentName, componentType, target string) *ExporterMetrics { + em := &ExporterMetrics{} + em.selfObservabilityEnabled = true + mp := otel.GetMeterProvider() + m := mp.Meter( + name, + metric.WithInstrumentationVersion(sdk.Version()), + metric.WithSchemaURL(semconv.SchemaURL)) + + var err error + if em.logInflightMetric, err = otelconv.NewSDKExporterLogInflight(m); err != nil { + otel.Handle(err) + } + if em.logExportedMetric, err = otelconv.NewSDKExporterLogExported(m); err != nil { + otel.Handle(err) + } + if em.logExportedDurationMetric, err = otelconv.NewSDKExporterOperationDuration(m); err != nil { + otel.Handle(err) + } + + em.presetAttrs = []attribute.KeyValue{ + semconv.OTelComponentName(componentName), + semconv.OTelComponentTypeKey.String(componentType), + } + em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) + return em +} + +func (em *ExporterMetrics) TrackExport(ctx context.Context) func(err error, code int) { + if !em.selfObservabilityEnabled { + return func(err error, code int) {} + } + + begin := time.Now() + em.logInflightMetric.Add(ctx, 1, em.presetAttrs...) + return func(err error, code int) { + duration := time.Since(begin).Seconds() + em.logInflightMetric.Add(ctx, -1, em.presetAttrs...) + if err != nil { + em.presetAttrs = append(em.presetAttrs, semconv.ErrorType(err)) + } + em.logExportedMetric.Add(ctx, 1, em.presetAttrs...) + em.presetAttrs = append( + em.presetAttrs, + em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), + ) + em.logExportedDurationMetric.Record(ctx, duration, em.presetAttrs...) + } +} + +func ServerAddrAttrs(target string) []attribute.KeyValue { + if strings.HasPrefix(target, "unix://") { + path := strings.TrimPrefix(target, "unix://") + return []attribute.KeyValue{semconv.ServerAddress(path)} + } + + if idx := strings.Index(target, "://"); idx != -1 { + target = target[idx+4:] + } + + host, pStr, err := net.SplitHostPort(target) + if err != nil { + return []attribute.KeyValue{semconv.ServerAddress(target)} + } + + port, err := strconv.Atoi(pStr) + if err != nil { + return []attribute.KeyValue{semconv.ServerAddress(host)} + } + return []attribute.KeyValue{ + semconv.ServerAddress(host), + semconv.ServerPort(port), + } +} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go new file mode 100644 index 00000000000..9cee0c45f22 --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" + +import ( + "testing" + + "go.opentelemetry.io/otel/attribute" + + "github.com/stretchr/testify/assert" + + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" +) + +func TestServerAddrAttrs(t *testing.T) { + testcases := []struct { + name string + target string + want []attribute.KeyValue + }{ + { + name: "UnixSocket", + target: "unix:///tmp/grpc.sock", + want: []attribute.KeyValue{semconv.ServerAddress("/tmp/grpc.sock")}, + }, + { + name: "DNSWithPort", + target: "dns:///localhost:8080", + want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(8080)}, + }, + { + name: "SimpleHostPort", + target: "localhost:10001", + want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(10001)}, + }, + { + name: "HostWithoutPort", + target: "example.com", + want: []attribute.KeyValue{semconv.ServerAddress("example.com")}, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + attrs := ServerAddrAttrs(tc.target) + assert.Equal(t, tc.want, attrs) + }) + } +} From 2f3ec9396d5667c759e30d11b288e79e139f86cd Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 5 Aug 2025 21:21:30 +0800 Subject: [PATCH 23/42] fix lint --- .../otlploggrpc/internal/selfobservability/selfobservability.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 5f902771afa..1807326e491 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -58,7 +58,7 @@ func NewExporterMetrics(name, componentName, componentType, target string) *Expo func (em *ExporterMetrics) TrackExport(ctx context.Context) func(err error, code int) { if !em.selfObservabilityEnabled { - return func(err error, code int) {} + return func(_ error, _ int) {} } begin := time.Now() From 289c3870296ea1baa2fa95b08edb6676fd246922 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 6 Aug 2025 16:18:34 +0800 Subject: [PATCH 24/42] fix(metrics): use log count in metric --- exporters/otlp/otlplog/otlploggrpc/client.go | 16 +++++++++------- .../otlp/otlplog/otlploggrpc/client_test.go | 6 +++--- .../selfobservability/selfobservability.go | 18 +++++++++++------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 5f6c665b4bd..8c3137b38f6 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -10,8 +10,6 @@ import ( "sync/atomic" "time" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" - collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" logpb "go.opentelemetry.io/proto/otlp/logs/v1" "google.golang.org/genproto/googleapis/rpc/errdetails" @@ -26,6 +24,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) @@ -93,10 +92,13 @@ func (c *client) initSelfObservability() { c.selfObservabilityEnabled = true id := grpcExporterIDCounter.Add(1) - 1 componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) + c.exporterMetric = selfobservability.NewExporterMetrics( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", componentName, - string(otelconv.ComponentTypeOtlpGRPCLogExporter), c.conn.Target()) + otelconv.ComponentTypeOtlpGRPCLogExporter, + c.conn.Target(), + ) } func newGRPCDialOptions(cfg config) []grpc.DialOption { @@ -157,7 +159,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error defer cancel() return c.requestFunc(ctx, func(ctx context.Context) error { - trackExportFunc := c.trackExport(context.Background()) + trackExportFunc := c.trackExport(context.Background(), int64(len(rl))) resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ ResourceLogs: rl, @@ -184,11 +186,11 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error }) } -func (c *client) trackExport(ctx context.Context) func(err error, code int) { +func (c *client) trackExport(ctx context.Context, count int64) func(err error, code int) { if !c.selfObservabilityEnabled { - return func(_ error, _ int) {} + return func(error, int) {} } - return c.exporterMetric.TrackExport(ctx) + return c.exporterMetric.TrackExport(ctx, count) } // Shutdown shuts down the client, freeing all resources. diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 097a7c46652..7c989f7ac7b 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -673,7 +673,7 @@ func TestSelfObservability(t *testing.T) { serverAddrAttrs[0], serverAddrAttrs[1], ), - Value: 1, + Value: int64(len(resourceLogs)), }, }, }, @@ -790,7 +790,7 @@ func TestSelfObservability(t *testing.T) { serverAddrAttrs[0], serverAddrAttrs[1], ), - Value: 1, + Value: int64(len(resourceLogs)), }, }, }, @@ -911,7 +911,7 @@ func TestSelfObservability(t *testing.T) { serverAddrAttrs[1], wantErrTypeAttr, ), - Value: 1, + Value: int64(len(resourceLogs)), }, }, }, diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 1807326e491..7240994eaa1 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -28,7 +28,11 @@ type ExporterMetrics struct { presetAttrs []attribute.KeyValue } -func NewExporterMetrics(name, componentName, componentType, target string) *ExporterMetrics { +func NewExporterMetrics( + name, componentName string, + componentType otelconv.ComponentTypeAttr, + target string, +) *ExporterMetrics { em := &ExporterMetrics{} em.selfObservabilityEnabled = true mp := otel.GetMeterProvider() @@ -50,26 +54,26 @@ func NewExporterMetrics(name, componentName, componentType, target string) *Expo em.presetAttrs = []attribute.KeyValue{ semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(componentType), + semconv.OTelComponentTypeKey.String(string(componentType)), } em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) return em } -func (em *ExporterMetrics) TrackExport(ctx context.Context) func(err error, code int) { +func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error, code int) { if !em.selfObservabilityEnabled { - return func(_ error, _ int) {} + return func(error, int) {} } begin := time.Now() - em.logInflightMetric.Add(ctx, 1, em.presetAttrs...) + em.logInflightMetric.Add(ctx, count, em.presetAttrs...) return func(err error, code int) { duration := time.Since(begin).Seconds() - em.logInflightMetric.Add(ctx, -1, em.presetAttrs...) + em.logInflightMetric.Add(ctx, -count, em.presetAttrs...) if err != nil { em.presetAttrs = append(em.presetAttrs, semconv.ErrorType(err)) } - em.logExportedMetric.Add(ctx, 1, em.presetAttrs...) + em.logExportedMetric.Add(ctx, count, em.presetAttrs...) em.presetAttrs = append( em.presetAttrs, em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), From b5c565be3872275dad621f8d4e5e3892c7734dd5 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 6 Aug 2025 20:07:35 +0800 Subject: [PATCH 25/42] rm the duplicate definition --- .../otlp/otlplog/otlploggrpc/client_test.go | 18 ++++++++---------- .../selfobservability/selfobservability.go | 6 ------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index e5eb7214010..598fab5fc67 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -11,12 +11,6 @@ import ( "testing" "time" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" - "go.opentelemetry.io/otel/sdk" - "go.opentelemetry.io/otel/sdk/instrumentation" - "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -34,13 +28,17 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" - "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" - "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/log" + "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.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) var ( diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 7240994eaa1..de55398c4b0 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -21,7 +21,6 @@ import ( ) type ExporterMetrics struct { - selfObservabilityEnabled bool logInflightMetric otelconv.SDKExporterLogInflight logExportedMetric otelconv.SDKExporterLogExported logExportedDurationMetric otelconv.SDKExporterOperationDuration @@ -34,7 +33,6 @@ func NewExporterMetrics( target string, ) *ExporterMetrics { em := &ExporterMetrics{} - em.selfObservabilityEnabled = true mp := otel.GetMeterProvider() m := mp.Meter( name, @@ -61,10 +59,6 @@ func NewExporterMetrics( } func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error, code int) { - if !em.selfObservabilityEnabled { - return func(error, int) {} - } - begin := time.Now() em.logInflightMetric.Add(ctx, count, em.presetAttrs...) return func(err error, code int) { From 9802efcdc46a4cb8a9ffe8afe9ca41cf3b96f430 Mon Sep 17 00:00:00 2001 From: yumosx Date: Sun, 10 Aug 2025 00:11:47 +0800 Subject: [PATCH 26/42] use loacl attrs and added the test for initselfobservability --- .../otlp/otlplog/otlploggrpc/client_test.go | 37 +++++++++++++++++++ .../selfobservability/selfobservability.go | 16 ++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 598fab5fc67..b4f54c73526 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -610,6 +610,43 @@ func TestConfig(t *testing.T) { }) } +func TestInitSelfObservability(t *testing.T) { + conn, err := grpc.NewClient("test", grpc.WithTransportCredentials(insecure.NewCredentials())) + t.Cleanup(func() { _ = conn.Close() }) + + require.NoError(t, err) + + testcases := []struct { + name string + before func() + want *selfobservability.ExporterMetrics + }{ + { + name: "disable self observability", + before: func() { t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "False") }, + }, + { + name: "enable self observability", + before: func() { t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True") }, + want: selfobservability.NewExporterMetrics( + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", + fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, 0), + otelconv.ComponentTypeOtlpGRPCLogExporter, + conn.CanonicalTarget()), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc.before() + + c := &client{conn: conn} + c.initSelfObservability() + assert.Equal(t, tc.want, c.exporterMetric) + }) + } +} + func TestSelfObservability(t *testing.T) { testCases := []struct { name string diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 6f46c740ce3..7379352696b 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -61,20 +61,22 @@ func NewExporterMetrics( } func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error, code codes.Code) { + attrs := append([]attribute.KeyValue{}, em.presetAttrs...) + begin := time.Now() - em.logInflightMetric.Add(ctx, count, em.presetAttrs...) + em.logInflightMetric.Add(ctx, count, attrs...) return func(err error, code codes.Code) { duration := time.Since(begin).Seconds() - em.logInflightMetric.Add(ctx, -count, em.presetAttrs...) + em.logInflightMetric.Add(ctx, -count, attrs...) if err != nil { - em.presetAttrs = append(em.presetAttrs, semconv.ErrorType(err)) + attrs = append(attrs, semconv.ErrorType(err)) } - em.logExportedMetric.Add(ctx, count, em.presetAttrs...) - em.presetAttrs = append( - em.presetAttrs, + em.logExportedMetric.Add(ctx, count, attrs...) + attrs = append( + attrs, em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), ) - em.logExportedDurationMetric.Record(ctx, duration, em.presetAttrs...) + em.logExportedDurationMetric.Record(ctx, duration, attrs...) } } From c77c78527187931b0e9828fc33e2db7f709f6519 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 11 Aug 2025 17:29:50 +0800 Subject: [PATCH 27/42] Fix the issue of repeated exports. --- exporters/otlp/otlplog/otlploggrpc/client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 5a6964ff686..a97d4d3d226 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -158,9 +158,8 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error ctx, cancel := c.exportContext(ctx) defer cancel() + trackExportFunc := c.trackExport(context.Background(), int64(len(rl))) return c.requestFunc(ctx, func(ctx context.Context) error { - trackExportFunc := c.trackExport(context.Background(), int64(len(rl))) - resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ ResourceLogs: rl, }) From 86a3074258743ab9a289655ceda93d1c4c4201a8 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 12 Aug 2025 17:56:11 +0800 Subject: [PATCH 28/42] move the trackExportfunc to outside --- exporters/otlp/otlplog/otlploggrpc/client.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index a97d4d3d226..21d84ea1d24 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -159,7 +159,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error defer cancel() trackExportFunc := c.trackExport(context.Background(), int64(len(rl))) - return c.requestFunc(ctx, func(ctx context.Context) error { + err := c.requestFunc(ctx, func(ctx context.Context) error { resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ ResourceLogs: rl, }) @@ -169,20 +169,22 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error n := resp.PartialSuccess.GetRejectedLogRecords() if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) - trackExportFunc(err, status.Code(err)) otel.Handle(err) - return nil } } // nil is converted to OK. if status.Code(err) == codes.OK { // Success. - trackExportFunc(nil, codes.OK) return nil } - trackExportFunc(err, status.Code(err)) return err }) + if err != nil { + trackExportFunc(err, status.Code(err)) + } else { + trackExportFunc(nil, codes.OK) + } + return err } func (c *client) trackExport(ctx context.Context, count int64) func(err error, code codes.Code) { From 159c0c19e963ce0222e78fa27693ae2a19546643 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 12 Aug 2025 18:24:59 +0800 Subject: [PATCH 29/42] fix conflicts --- exporters/otlp/otlplog/otlploggrpc/client.go | 5 +++-- exporters/otlp/otlplog/otlploggrpc/client_test.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 21d84ea1d24..6b63091c322 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -169,11 +169,14 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error n := resp.PartialSuccess.GetRejectedLogRecords() if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) + trackExportFunc(err, codes.OK) otel.Handle(err) + return nil } } // nil is converted to OK. if status.Code(err) == codes.OK { + trackExportFunc(nil, codes.OK) // Success. return nil } @@ -181,8 +184,6 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error }) if err != nil { trackExportFunc(err, status.Code(err)) - } else { - trackExportFunc(nil, codes.OK) } return err } diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index b4f54c73526..54de4a38c45 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -845,7 +845,7 @@ func TestSelfObservability(t *testing.T) { ), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr( - status.Code(wantErr), + codes.OK, ), ), semconv.ErrorType(wantErr), From 921f39085cc8cf01ee3fe85d07c522fda8473bbe Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 18 Aug 2025 20:53:24 +0800 Subject: [PATCH 30/42] refactor(selfobservability): improve server address parsing and track_export --- exporters/otlp/otlplog/otlploggrpc/client.go | 39 +++-- .../otlp/otlplog/otlploggrpc/client_test.go | 138 +++++++----------- .../selfobservability/selfobservability.go | 72 +++++++-- .../selfobservability_test.go | 5 + .../otlplog/otlploggrpc/internal/x/README.md | 2 +- 5 files changed, 137 insertions(+), 119 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 6b63091c322..13a42757ca1 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -29,8 +29,6 @@ import ( "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) -var grpcExporterIDCounter atomic.Int64 - // The methods of this type are not expected to be called concurrently. type client struct { metadata metadata.MD @@ -90,7 +88,7 @@ func (c *client) initSelfObservability() { } c.selfObservabilityEnabled = true - id := grpcExporterIDCounter.Add(1) - 1 + id := nextExporterID() componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) c.exporterMetric = selfobservability.NewExporterMetrics( @@ -147,7 +145,7 @@ func newGRPCDialOptions(cfg config) []grpc.DialOption { // The otlplog.Exporter synchronizes access to client methods, and // ensures this is not called after the Exporter is shutdown. Only thing // to do here is send data. -func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error { +func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) (err error) { select { case <-ctx.Done(): // Do not upload if the context is already expired. @@ -158,8 +156,14 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error ctx, cancel := c.exportContext(ctx) defer cancel() - trackExportFunc := c.trackExport(context.Background(), int64(len(rl))) - err := c.requestFunc(ctx, func(ctx context.Context) error { + success := int64(len(rl)) + if c.selfObservabilityEnabled { + count := len(rl) + trackExportFunc := c.exporterMetric.TrackExport(context.Background(), int64(count)) + defer func() { trackExportFunc(err, success, status.Code(err)) }() + } + + return c.requestFunc(ctx, func(ctx context.Context) error { resp, err := c.lsc.Export(ctx, &collogpb.ExportLogsServiceRequest{ ResourceLogs: rl, }) @@ -167,32 +171,19 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) error if resp != nil && resp.PartialSuccess != nil { msg := resp.PartialSuccess.GetErrorMessage() n := resp.PartialSuccess.GetRejectedLogRecords() + success -= n if n != 0 || msg != "" { err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) - trackExportFunc(err, codes.OK) otel.Handle(err) - return nil } } // nil is converted to OK. if status.Code(err) == codes.OK { - trackExportFunc(nil, codes.OK) // Success. return nil } return err }) - if err != nil { - trackExportFunc(err, status.Code(err)) - } - return err -} - -func (c *client) trackExport(ctx context.Context, count int64) func(err error, code codes.Code) { - if !c.selfObservabilityEnabled { - return func(error, codes.Code) {} - } - return c.exporterMetric.TrackExport(ctx, count) } // Shutdown shuts down the client, freeing all resources. @@ -302,3 +293,11 @@ func throttleDelay(s *status.Status) (bool, time.Duration) { } return false, 0 } + +var grpcExporterIDCounter atomic.Int64 + +// nextExporterID returns a new unique ID for an exporter. +// the starting value is 0, and it increments by 1 for each call. +func nextExporterID() int64 { + return grpcExporterIDCounter.Add(1) - 1 +} diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 54de4a38c45..bc5015002ba 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -610,60 +610,33 @@ func TestConfig(t *testing.T) { }) } -func TestInitSelfObservability(t *testing.T) { - conn, err := grpc.NewClient("test", grpc.WithTransportCredentials(insecure.NewCredentials())) - t.Cleanup(func() { _ = conn.Close() }) - - require.NoError(t, err) - - testcases := []struct { - name string - before func() - want *selfobservability.ExporterMetrics - }{ - { - name: "disable self observability", - before: func() { t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "False") }, - }, - { - name: "enable self observability", - before: func() { t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True") }, - want: selfobservability.NewExporterMetrics( - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", - fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, 0), - otelconv.ComponentTypeOtlpGRPCLogExporter, - conn.CanonicalTarget()), - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - tc.before() - - c := &client{conn: conn} - c.initSelfObservability() - assert.Equal(t, tc.want, c.exporterMetric) - }) - } -} - func TestSelfObservability(t *testing.T) { testCases := []struct { - name string - test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) + name string + enabled bool + test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) }{ { - name: "upload success", + name: "disable", + enabled: false, + test: func(t *testing.T, _ func() metricdata.ScopeMetrics) { + client, _ := clientFactory(t, nil) + assert.Empty(t, client.exporterMetric) + }, + }, + { + name: "upload success", + enabled: true, test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { ctx := context.Background() client, coll := clientFactory(t, nil) componentName := fmt.Sprintf( "%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, - grpcExporterIDCounter.Load()-1, + 0, ) serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) - want := metricdata.ScopeMetrics{ + wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", Version: sdk.Version(), @@ -751,12 +724,20 @@ func TestSelfObservability(t *testing.T) { t.Fatalf("unexpected ResourceLogs:\n%s", diff) } g := scopeMetrics() - normalizeMetrics(&g) - metricdatatest.AssertEqual(t, want, g, metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, wantMetrics.Metrics[0], g.Metrics[0], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, wantMetrics.Metrics[1], g.Metrics[1], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual( + t, + wantMetrics.Metrics[2], + g.Metrics[2], + metricdatatest.IgnoreTimestamp(), + metricdatatest.IgnoreValue(), + ) }, }, { - name: "partial success", + name: "partial success", + enabled: true, test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { const n, msg = 2, "bad data" rCh := make(chan exportResult, 1) @@ -774,9 +755,8 @@ func TestSelfObservability(t *testing.T) { componentName := fmt.Sprintf( "%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, - grpcExporterIDCounter.Load()-1, + 1, ) - wantErr := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ @@ -821,11 +801,10 @@ func TestSelfObservability(t *testing.T) { otelconv.SDKExporterLogExported{}.AttrComponentType( otelconv.ComponentTypeOtlpGRPCLogExporter, ), - semconv.ErrorType(wantErr), serverAddrAttrs[0], serverAddrAttrs[1], ), - Value: int64(len(resourceLogs)), + Value: int64(len(resourceLogs)) - 2, }, }, }, @@ -848,7 +827,6 @@ func TestSelfObservability(t *testing.T) { codes.OK, ), ), - semconv.ErrorType(wantErr), serverAddrAttrs[0], serverAddrAttrs[1], ), @@ -875,12 +853,20 @@ func TestSelfObservability(t *testing.T) { assert.ErrorContains(t, errs[0], want) g := scopeMetrics() - normalizeMetrics(&g) - metricdatatest.AssertEqual(t, wantMetrics, g, metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, wantMetrics.Metrics[0], g.Metrics[0], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, wantMetrics.Metrics[1], g.Metrics[1], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual( + t, + wantMetrics.Metrics[2], + g.Metrics[2], + metricdatatest.IgnoreTimestamp(), + metricdatatest.IgnoreValue(), + ) }, }, { - name: "upload failure", + name: "upload failure", + enabled: true, test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { wantErr := status.Error(codes.InvalidArgument, "request contains invalid arguments") wantErrTypeAttr := semconv.ErrorType(wantErr) @@ -897,7 +883,7 @@ func TestSelfObservability(t *testing.T) { componentName := fmt.Sprintf( "%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, - grpcExporterIDCounter.Load()-1, + 2, ) serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) wantMetrics := metricdata.ScopeMetrics{ @@ -946,7 +932,7 @@ func TestSelfObservability(t *testing.T) { serverAddrAttrs[1], wantErrTypeAttr, ), - Value: int64(len(resourceLogs)), + Value: 0, }, }, }, @@ -979,16 +965,27 @@ func TestSelfObservability(t *testing.T) { }, } g := scopeMetrics() - normalizeMetrics(&g) - metricdatatest.AssertEqual(t, wantMetrics, g, metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, wantMetrics.Metrics[0], g.Metrics[0], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, wantMetrics.Metrics[1], g.Metrics[1], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual( + t, + wantMetrics.Metrics[2], + g.Metrics[2], + metricdatatest.IgnoreTimestamp(), + metricdatatest.IgnoreValue(), + ) }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True") + if tc.enabled { + t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True") + } prev := otel.GetMeterProvider() - defer otel.SetMeterProvider(prev) + t.Cleanup(func() { + otel.SetMeterProvider(prev) + }) r := metric.NewManualReader() mp := metric.NewMeterProvider(metric.WithReader(r)) otel.SetMeterProvider(mp) @@ -1004,26 +1001,3 @@ func TestSelfObservability(t *testing.T) { }) } } - -func normalizeMetrics(scopeMetrics *metricdata.ScopeMetrics) { - for i := range scopeMetrics.Metrics { - m := &scopeMetrics.Metrics[i] - if data, ok := m.Data.(metricdata.Histogram[float64]); ok { - name := otelconv.SDKExporterOperationDuration{}.Name() - if m.Name != name { - break - } - for j := range data.DataPoints { - dp := &data.DataPoints[j] - dp.StartTime = time.Time{} - dp.Time = time.Time{} - dp.Min = metricdata.Extrema[float64]{} - dp.Max = metricdata.Extrema[float64]{} - dp.Bounds = nil - dp.BucketCounts = nil - dp.Sum = 0 - } - m.Data = data - } - } -} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 7379352696b..77a7e7351c1 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -8,8 +8,10 @@ package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otl import ( "context" "net" + "net/url" "strconv" "strings" + "sync" "time" "google.golang.org/grpc/codes" @@ -22,6 +24,15 @@ import ( "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) +var attrsPool = sync.Pool{ + New: func() any { + // "component.name" + "component.type" + "error.type" + "target" + const n = 1 + 1 + 1 + 1 + s := make([]attribute.KeyValue, 0, n) + return &s + }, +} + type ExporterMetrics struct { logInflightMetric otelconv.SDKExporterLogInflight logExportedMetric otelconv.SDKExporterLogExported @@ -60,41 +71,70 @@ func NewExporterMetrics( return em } -func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error, code codes.Code) { - attrs := append([]attribute.KeyValue{}, em.presetAttrs...) +func (em *ExporterMetrics) TrackExport( + ctx context.Context, + count int64, +) func(err error, successCount int64, code codes.Code) { + attrs := attrsPool.Get().(*[]attribute.KeyValue) + *attrs = append([]attribute.KeyValue{}, em.presetAttrs...) begin := time.Now() - em.logInflightMetric.Add(ctx, count, attrs...) - return func(err error, code codes.Code) { + em.logInflightMetric.Add(ctx, count, *attrs...) + return func(err error, successCount int64, code codes.Code) { + defer func() { + *attrs = (*attrs)[:0] + attrsPool.Put(attrs) + }() + duration := time.Since(begin).Seconds() - em.logInflightMetric.Add(ctx, -count, attrs...) + em.logInflightMetric.Add(ctx, -count, *attrs...) + if err != nil { - attrs = append(attrs, semconv.ErrorType(err)) + *attrs = append(*attrs, semconv.ErrorType(err)) + em.logExportedMetric.Add(ctx, 0, *attrs...) + } else { + em.logExportedMetric.Add(ctx, successCount, *attrs...) } - em.logExportedMetric.Add(ctx, count, attrs...) - attrs = append( - attrs, + + *attrs = append( + *attrs, em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), ) - em.logExportedDurationMetric.Record(ctx, duration, attrs...) + em.logExportedDurationMetric.Record(ctx, duration, *attrs...) } } func ServerAddrAttrs(target string) []attribute.KeyValue { - if strings.HasPrefix(target, "unix://") { - path := strings.TrimPrefix(target, "unix://") - return []attribute.KeyValue{semconv.ServerAddress(path)} + if !strings.Contains(target, "://") { + return splitHostPortAttrs(target) } - if idx := strings.Index(target, "://"); idx != -1 { - target = target[idx+4:] + u, err := url.Parse(target) + if err != nil || u.Scheme == "" { + return splitHostPortAttrs(target) } + switch u.Scheme { + case "unix": + // unix:///path/to/socket + return []attribute.KeyValue{semconv.ServerAddress(u.Path)} + case "dns": + // dns:///example.com:42 or dns://8.8.8.8/example.com:42 + addr := u.Opaque + if addr == "" { + addr = strings.TrimPrefix(u.Path, "/") + } + return splitHostPortAttrs(addr) + default: + return splitHostPortAttrs(u.Host) + } +} + +func splitHostPortAttrs(target string) []attribute.KeyValue { host, pStr, err := net.SplitHostPort(target) if err != nil { return []attribute.KeyValue{semconv.ServerAddress(target)} } - port, err := strconv.Atoi(pStr) if err != nil { return []attribute.KeyValue{semconv.ServerAddress(host)} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go index 7c3bfaa94a3..77c9d5b0673 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go @@ -29,6 +29,11 @@ func TestServerAddrAttrs(t *testing.T) { target: "dns:///localhost:8080", want: []attribute.KeyValue{semconv.ServerAddress("localhost"), semconv.ServerPort(8080)}, }, + { + name: "Dns with endpoint host:port", + target: "dns://8.8.8.8/example.com:4", + want: []attribute.KeyValue{semconv.ServerAddress("example.com"), semconv.ServerPort(4)}, + }, { name: "Simple host port", target: "localhost:10001", diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md index 326e6bc5b80..bc8e580597e 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md +++ b/exporters/otlp/otlplog/otlploggrpc/internal/x/README.md @@ -3,7 +3,7 @@ The `otlploggrpc` exporter contains features that have not yet stabilized in the OpenTelemetry specification. These features are added to the `otlploggrpc` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback. -These feature may change in backwards incompatible ways as feedback is applied. +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 From 33cf67c93eb71c6568a5686e7a7f4d4f2b236cd3 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 18 Aug 2025 21:15:51 +0800 Subject: [PATCH 31/42] Restructure component ID counting --- exporters/otlp/otlplog/otlploggrpc/client.go | 12 +--- .../otlploggrpc/internal/counter/counter.go | 30 +++++++++ .../internal/counter/counter_test.go | 62 +++++++++++++++++++ 3 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/counter/counter.go create mode 100644 exporters/otlp/otlplog/otlploggrpc/internal/counter/counter_test.go diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 13a42757ca1..592a2d71b57 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "sync/atomic" "time" collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" @@ -23,6 +22,7 @@ import ( "google.golang.org/grpc/status" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/counter" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" @@ -88,7 +88,7 @@ func (c *client) initSelfObservability() { } c.selfObservabilityEnabled = true - id := nextExporterID() + id := counter.NextExporterID() componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) c.exporterMetric = selfobservability.NewExporterMetrics( @@ -293,11 +293,3 @@ func throttleDelay(s *status.Status) (bool, time.Duration) { } return false, 0 } - -var grpcExporterIDCounter atomic.Int64 - -// nextExporterID returns a new unique ID for an exporter. -// the starting value is 0, and it increments by 1 for each call. -func nextExporterID() int64 { - return grpcExporterIDCounter.Add(1) - 1 -} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/counter/counter.go b/exporters/otlp/otlplog/otlploggrpc/internal/counter/counter.go new file mode 100644 index 00000000000..af1be225231 --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/counter/counter.go @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package counter provides a simple counter for generating unique IDs. +// +// This package is used to generate unique IDs while allowing testing packages +// to reset the counter. +package counter // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/counter" + +import ( + "sync/atomic" +) + +// exporterN is a global 0-based count of the number of exporters created. +var exporterN atomic.Int64 + +// NextExporterID returns the next unique ID for an exporter. +func NextExporterID() int64 { + const inc = 1 + return exporterN.Add(inc) - inc +} + +// SetExporterID sets the exporter ID counter to v and returns the previous +// value. +// +// This function is useful for testing purposes, allowing you to reset the +// counter. It should not be used in production code. +func SetExporterID(v int64) int64 { + return exporterN.Swap(v) +} diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/counter/counter_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/counter/counter_test.go new file mode 100644 index 00000000000..787216e3bbc --- /dev/null +++ b/exporters/otlp/otlplog/otlploggrpc/internal/counter/counter_test.go @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package counter + +import ( + "sync" + "testing" +) + +func TestNextExporterID(t *testing.T) { + SetExporterID(0) + + var expected int64 + for range 10 { + id := NextExporterID() + if id != expected { + t.Errorf("NextExporterID() = %d; want %d", id, expected) + } + expected++ + } +} + +func TestSetExporterID(t *testing.T) { + SetExporterID(0) + + prev := SetExporterID(42) + if prev != 0 { + t.Errorf("SetExporterID(42) returned %d; want 0", prev) + } + + id := NextExporterID() + if id != 42 { + t.Errorf("NextExporterID() = %d; want 42", id) + } +} + +func TestNextExporterIDConcurrentSafe(t *testing.T) { + SetExporterID(0) + + const goroutines = 100 + const increments = 10 + + var wg sync.WaitGroup + wg.Add(goroutines) + + for range goroutines { + go func() { + defer wg.Done() + for range increments { + NextExporterID() + } + }() + } + + wg.Wait() + + expected := int64(goroutines * increments) + if id := NextExporterID(); id != expected { + t.Errorf("NextExporterID() = %d; want %d", id, expected) + } +} From a02787833b8e29d6550a9cc04e9b16f06cc5decd Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 19 Aug 2025 01:23:27 +0800 Subject: [PATCH 32/42] fix changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd11bd6ca2..e835aecd4a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ The next release will require at least [Go 1.24]. - Add experimental self-observability trace exporter metrics in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`. Check the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace/internal/x` package documentation for more information. (#7133) - Support testing of [Go 1.25]. (#7187) +- Add experimental self-observability metrics in go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc. + Check the go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x package documentation for more information. (#7084) ### Changed From cc2d577e01c89a32737543896a6b0735cf4dc8bb Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 20 Aug 2025 16:00:24 +0800 Subject: [PATCH 33/42] refactor(metrics): add missing error export counter for partial success --- CHANGELOG.md | 4 +- exporters/otlp/otlplog/otlploggrpc/client.go | 34 ++++++------ .../otlp/otlplog/otlploggrpc/client_test.go | 32 ++++++++++-- .../selfobservability/selfobservability.go | 52 +++++++++++-------- 4 files changed, 78 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e835aecd4a0..165fa547ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,8 +57,8 @@ The next release will require at least [Go 1.24]. - Add experimental self-observability trace exporter metrics in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`. Check the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace/internal/x` package documentation for more information. (#7133) - Support testing of [Go 1.25]. (#7187) -- Add experimental self-observability metrics in go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc. - Check the go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x package documentation for more information. (#7084) +- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. + Check the `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x` package documentation for more information. (#7084) ### Changed diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 592a2d71b57..7320ad043f7 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -43,8 +43,7 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient - selfObservabilityEnabled bool - exporterMetric *selfobservability.ExporterMetrics + exporterMetric *selfobservability.ExporterMetrics } // Used for testing. @@ -78,25 +77,21 @@ func newClient(cfg config) (*client, error) { } c.lsc = collogpb.NewLogsServiceClient(c.conn) - c.initSelfObservability() - return c, nil -} -func (c *client) initSelfObservability() { if !x.SelfObservability.Enabled() { - return + return c, nil } - c.selfObservabilityEnabled = true id := counter.NextExporterID() componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) - - c.exporterMetric = selfobservability.NewExporterMetrics( + var err error + c.exporterMetric, err = selfobservability.NewExporterMetrics( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", componentName, otelconv.ComponentTypeOtlpGRPCLogExporter, c.conn.CanonicalTarget(), ) + return c, err } func newGRPCDialOptions(cfg config) []grpc.DialOption { @@ -157,10 +152,18 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) (err defer cancel() success := int64(len(rl)) - if c.selfObservabilityEnabled { + // partialSuccessErr records an error when the export is partially successful. + var partialSuccessErr error + if c.exporterMetric != nil { count := len(rl) - trackExportFunc := c.exporterMetric.TrackExport(context.Background(), int64(count)) - defer func() { trackExportFunc(err, success, status.Code(err)) }() + trackExportFunc := c.exporterMetric.TrackExport(ctx, int64(count)) + defer func() { + if partialSuccessErr != nil { + trackExportFunc(partialSuccessErr, success, status.Code(partialSuccessErr)) + return + } + trackExportFunc(err, success, status.Code(err)) + }() } return c.requestFunc(ctx, func(ctx context.Context) error { @@ -173,8 +176,8 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) (err n := resp.PartialSuccess.GetRejectedLogRecords() success -= n if n != 0 || msg != "" { - err := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) - otel.Handle(err) + partialSuccessErr = fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) + otel.Handle(partialSuccessErr) } } // nil is converted to OK. @@ -182,6 +185,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) (err // Success. return nil } + success = 0 return err }) } diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index bc5015002ba..916daf7577a 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -758,6 +758,7 @@ func TestSelfObservability(t *testing.T) { 1, ) serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) + wantErr := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -806,6 +807,18 @@ func TestSelfObservability(t *testing.T) { ), Value: int64(len(resourceLogs)) - 2, }, + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), + otelconv.SDKExporterLogExported{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + serverAddrAttrs[0], + serverAddrAttrs[1], + semconv.ErrorType(wantErr), + ), + Value: 2, + }, }, }, }, @@ -824,11 +837,12 @@ func TestSelfObservability(t *testing.T) { ), otelconv.SDKExporterOperationDuration{}.AttrRPCGRPCStatusCode( otelconv.RPCGRPCStatusCodeAttr( - codes.OK, + status.Code(wantErr), ), ), serverAddrAttrs[0], serverAddrAttrs[1], + semconv.ErrorType(wantErr), ), Count: 1, }, @@ -849,8 +863,7 @@ func TestSelfObservability(t *testing.T) { require.NoError(t, client.UploadLogs(ctx, resourceLogs)) require.Len(t, errs, 1) - want := fmt.Sprintf("%s (%d log records rejected)", msg, n) - assert.ErrorContains(t, errs[0], want) + assert.ErrorContains(t, errs[0], wantErr.Error()) g := scopeMetrics() metricdatatest.AssertEqual(t, wantMetrics.Metrics[0], g.Metrics[0], metricdatatest.IgnoreTimestamp()) @@ -930,10 +943,21 @@ func TestSelfObservability(t *testing.T) { ), serverAddrAttrs[0], serverAddrAttrs[1], - wantErrTypeAttr, ), Value: 0, }, + { + Attributes: attribute.NewSet( + otelconv.SDKExporterLogExported{}.AttrComponentName(componentName), + otelconv.SDKExporterLogExported{}.AttrComponentType( + otelconv.ComponentTypeOtlpGRPCLogExporter, + ), + serverAddrAttrs[0], + serverAddrAttrs[1], + wantErrTypeAttr, + ), + Value: 1, + }, }, }, }, diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 77a7e7351c1..95c7b40b500 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -7,6 +7,8 @@ package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otl import ( "context" + "errors" + "fmt" "net" "net/url" "strconv" @@ -26,8 +28,8 @@ import ( var attrsPool = sync.Pool{ New: func() any { - // "component.name" + "component.type" + "error.type" + "target" - const n = 1 + 1 + 1 + 1 + // "component.name" + "component.type" + "error.type" + "server.address" + "server.port" + "rpc.grpc.status_code" + const n = 1 + 1 + 1 + 1 + 1 + 1 s := make([]attribute.KeyValue, 0, n) return &s }, @@ -44,31 +46,38 @@ func NewExporterMetrics( name, componentName string, componentType otelconv.ComponentTypeAttr, target string, -) *ExporterMetrics { +) (*ExporterMetrics, error) { em := &ExporterMetrics{} + em.presetAttrs = []attribute.KeyValue{ + semconv.OTelComponentName(componentName), + semconv.OTelComponentTypeKey.String(string(componentType)), + } + em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) + mp := otel.GetMeterProvider() m := mp.Meter( name, metric.WithInstrumentationVersion(sdk.Version()), - metric.WithSchemaURL(semconv.SchemaURL)) - - var err error - if em.logInflightMetric, err = otelconv.NewSDKExporterLogInflight(m); err != nil { - otel.Handle(err) + metric.WithSchemaURL(semconv.SchemaURL), + ) + + var err, e error + if em.logInflightMetric, e = otelconv.NewSDKExporterLogInflight(m); e != nil { + e = fmt.Errorf("failed to create span inflight metric: %w", e) + otel.Handle(e) + err = errors.Join(err, e) } - if em.logExportedMetric, err = otelconv.NewSDKExporterLogExported(m); err != nil { - otel.Handle(err) + if em.logExportedMetric, e = otelconv.NewSDKExporterLogExported(m); e != nil { + e = fmt.Errorf("failed to create span exported metric: %w", e) + otel.Handle(e) + err = errors.Join(err, e) } - if em.logExportedDurationMetric, err = otelconv.NewSDKExporterOperationDuration(m); err != nil { + if em.logExportedDurationMetric, e = otelconv.NewSDKExporterOperationDuration(m); e != nil { + e = fmt.Errorf("failed to create operation duration metric: %w", e) otel.Handle(err) + err = errors.Join(err, e) } - - em.presetAttrs = []attribute.KeyValue{ - semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(string(componentType)), - } - em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) - return em + return em, err } func (em *ExporterMetrics) TrackExport( @@ -88,14 +97,11 @@ func (em *ExporterMetrics) TrackExport( duration := time.Since(begin).Seconds() em.logInflightMetric.Add(ctx, -count, *attrs...) - + em.logExportedMetric.Add(ctx, successCount, *attrs...) if err != nil { *attrs = append(*attrs, semconv.ErrorType(err)) - em.logExportedMetric.Add(ctx, 0, *attrs...) - } else { - em.logExportedMetric.Add(ctx, successCount, *attrs...) + em.logExportedMetric.Add(ctx, count-successCount, *attrs...) } - *attrs = append( *attrs, em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), From 65776ba243ff0057c3c20c16c81c5f526004311c Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 20 Aug 2025 20:16:45 +0800 Subject: [PATCH 34/42] fix the NewExporterMetrics and add test --- .../selfobservability/selfobservability.go | 2 +- .../selfobservability_test.go | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 95c7b40b500..001a66d49bf 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -74,7 +74,7 @@ func NewExporterMetrics( } if em.logExportedDurationMetric, e = otelconv.NewSDKExporterOperationDuration(m); e != nil { e = fmt.Errorf("failed to create operation duration metric: %w", e) - otel.Handle(err) + otel.Handle(e) err = errors.Join(err, e) } return em, err diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go index 77c9d5b0673..d70469b9343 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go @@ -6,6 +6,11 @@ package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otl import ( "testing" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel" + mapi "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/attribute" "github.com/stretchr/testify/assert" @@ -13,6 +18,75 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.36.0" ) +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 TestNewExporterMetrics(t *testing.T) { + t.Run("No Error", func(t *testing.T) { + em, err := NewExporterMetrics( + "newExportMetricsTest", + "newExportMetricsTest/1", + "newExportMetricsTest", + "localhost:8080", + ) + require.NoError(t, err) + assert.ElementsMatch(t, []attribute.KeyValue{ + semconv.OTelComponentName("newExportMetricsTest/1"), + semconv.OTelComponentTypeKey.String("newExportMetricsTest"), + semconv.ServerAddress("localhost"), + semconv.ServerPort(8080), + }, em.presetAttrs) + + assert.NotNil(t, em.logInflightMetric, "logInflightMetric should be created") + assert.NotNil(t, em.logExportedMetric, "logExportedMetric should be created") + assert.NotNil(t, em.logExportedDurationMetric, "logExportedDurationMetric should be created") + }) + + t.Run("Error", func(t *testing.T) { + orig := otel.GetMeterProvider() + t.Cleanup(func() { otel.SetMeterProvider(orig) }) + mp := &errMeterProvider{err: assert.AnError} + otel.SetMeterProvider(mp) + + _, err := NewExporterMetrics( + "newExportMetrics", + "newExportMetrics/1", + "newExportMetrics", + "localhost:8080", + ) + require.ErrorIs(t, err, assert.AnError, "new instrument errors") + + assert.ErrorContains(t, err, "inflight metric") + assert.ErrorContains(t, err, "span exported metric") + assert.ErrorContains(t, err, "operation duration metric") + }) +} + func TestServerAddrAttrs(t *testing.T) { testcases := []struct { name string From 147363c48042f47e61f06c5a98038aed82be77b4 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 20 Aug 2025 20:23:11 +0800 Subject: [PATCH 35/42] fmt code --- .../internal/selfobservability/selfobservability_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go index d70469b9343..fd3abe627c5 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go @@ -6,15 +6,12 @@ package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otl import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" - mapi "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/attribute" - - "github.com/stretchr/testify/assert" - + mapi "go.opentelemetry.io/otel/metric" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" ) From d9e8dffe1ecadf33eca76ed6589777f87d1407bb Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 21 Aug 2025 01:23:16 +0800 Subject: [PATCH 36/42] refactor: avoid assigning presetAttrs when metric initialization fails --- .../selfobservability/selfobservability.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index 001a66d49bf..de80f1c0ef7 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -48,11 +48,6 @@ func NewExporterMetrics( target string, ) (*ExporterMetrics, error) { em := &ExporterMetrics{} - em.presetAttrs = []attribute.KeyValue{ - semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(string(componentType)), - } - em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) mp := otel.GetMeterProvider() m := mp.Meter( @@ -77,7 +72,17 @@ func NewExporterMetrics( otel.Handle(e) err = errors.Join(err, e) } - return em, err + + if err != nil { + return nil, err + } + + em.presetAttrs = []attribute.KeyValue{ + semconv.OTelComponentName(componentName), + semconv.OTelComponentTypeKey.String(string(componentType)), + } + em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) + return em, nil } func (em *ExporterMetrics) TrackExport( From a32dd6e7cbefbfc5af7720e76b3ae0345efe986d Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 28 Aug 2025 21:46:26 +0800 Subject: [PATCH 37/42] perf: use addSet instead of add for better performance --- .../internal/selfobservability/selfobservability.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index de80f1c0ef7..be7c3319b58 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -93,7 +93,7 @@ func (em *ExporterMetrics) TrackExport( *attrs = append([]attribute.KeyValue{}, em.presetAttrs...) begin := time.Now() - em.logInflightMetric.Add(ctx, count, *attrs...) + em.logInflightMetric.AddSet(ctx, count, attribute.NewSet(*attrs...)) return func(err error, successCount int64, code codes.Code) { defer func() { *attrs = (*attrs)[:0] @@ -101,17 +101,17 @@ func (em *ExporterMetrics) TrackExport( }() duration := time.Since(begin).Seconds() - em.logInflightMetric.Add(ctx, -count, *attrs...) - em.logExportedMetric.Add(ctx, successCount, *attrs...) + em.logInflightMetric.AddSet(ctx, -count, attribute.NewSet(*attrs...)) + em.logExportedMetric.AddSet(ctx, successCount, attribute.NewSet(*attrs...)) if err != nil { *attrs = append(*attrs, semconv.ErrorType(err)) - em.logExportedMetric.Add(ctx, count-successCount, *attrs...) + em.logExportedMetric.AddSet(ctx, count-successCount, attribute.NewSet(*attrs...)) } *attrs = append( *attrs, em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), ) - em.logExportedDurationMetric.Record(ctx, duration, *attrs...) + em.logExportedDurationMetric.RecordSet(ctx, duration, attribute.NewSet(*attrs...)) } } From b778294406bb5b690b12a2d292d6d2886edf2ae1 Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 28 Aug 2025 22:34:40 +0800 Subject: [PATCH 38/42] fix the missing tests --- exporters/otlp/otlplog/otlploggrpc/client.go | 2 +- exporters/otlp/otlplog/otlploggrpc/client_test.go | 4 ++++ .../internal/selfobservability/selfobservability.go | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 7320ad043f7..0431afa1a63 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -26,7 +26,7 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" + "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" ) // The methods of this type are not expected to be called concurrently. diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 45e1d048525..413e909ef3c 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -34,7 +34,11 @@ import ( "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/log" + "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" ) var ( diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go index be7c3319b58..5e0a9e9ec79 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go @@ -22,8 +22,8 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/sdk" - semconv "go.opentelemetry.io/otel/semconv/v1.36.0" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" ) var attrsPool = sync.Pool{ From ef06e5d1f4b8ee7720b2a442ed2ae5ff3e030f03 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 9 Sep 2025 23:41:51 +0800 Subject: [PATCH 39/42] update go mod --- exporters/otlp/otlplog/otlploggrpc/go.mod | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/exporters/otlp/otlplog/otlploggrpc/go.mod b/exporters/otlp/otlplog/otlploggrpc/go.mod index ce2dd5e4920..2851607bc52 100644 --- a/exporters/otlp/otlplog/otlploggrpc/go.mod +++ b/exporters/otlp/otlplog/otlploggrpc/go.mod @@ -9,21 +9,13 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 github.com/google/go-cmp v0.7.0 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/log v0.13.0 - go.opentelemetry.io/otel/metric v1.37.0 - go.opentelemetry.io/otel/sdk v1.37.0 - go.opentelemetry.io/otel/sdk/log v0.13.0 - go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 - go.opentelemetry.io/otel/sdk/metric v1.37.0 - go.opentelemetry.io/otel/trace v1.37.0 - go.opentelemetry.io/proto/otlp v1.7.1 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 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/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 @@ -39,7 +31,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect From eb8493a187d281abf7021f7bc0715222a7a727ef Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 10 Sep 2025 22:42:13 +0800 Subject: [PATCH 40/42] refactor(self-observability): align code with style guide --- exporters/otlp/otlplog/otlploggrpc/client.go | 6 +- ...elfobservability.go => instrumentation.go} | 128 +++++++++++++----- ...bility_test.go => instrumentation_test.go} | 4 +- 3 files changed, 99 insertions(+), 39 deletions(-) rename exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/{selfobservability.go => instrumentation.go} (51%) rename exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/{selfobservability_test.go => instrumentation_test.go} (98%) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 0431afa1a63..20fdb87fc30 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -43,7 +43,7 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient - exporterMetric *selfobservability.ExporterMetrics + exporterMetric *selfobservability.Instrumentation } // Used for testing. @@ -85,7 +85,7 @@ func newClient(cfg config) (*client, error) { id := counter.NextExporterID() componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) var err error - c.exporterMetric, err = selfobservability.NewExporterMetrics( + c.exporterMetric, err = selfobservability.NewInstrumentation( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", componentName, otelconv.ComponentTypeOtlpGRPCLogExporter, @@ -156,7 +156,7 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) (err var partialSuccessErr error if c.exporterMetric != nil { count := len(rl) - trackExportFunc := c.exporterMetric.TrackExport(ctx, int64(count)) + trackExportFunc := c.exporterMetric.ExportSpans(ctx, int64(count)) defer func() { if partialSuccessErr != nil { trackExportFunc(partialSuccessErr, success, status.Code(partialSuccessErr)) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation.go similarity index 51% rename from exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go rename to exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation.go index 5e0a9e9ec79..5fa055057f3 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation.go @@ -26,28 +26,53 @@ import ( "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" ) -var attrsPool = sync.Pool{ - New: func() any { - // "component.name" + "component.type" + "error.type" + "server.address" + "server.port" + "rpc.grpc.status_code" - const n = 1 + 1 + 1 + 1 + 1 + 1 - s := make([]attribute.KeyValue, 0, n) - return &s - }, +var ( + attrsPool = &sync.Pool{ + New: func() any { + // "component.name" + "component.type" + "error.type" + "server.address" + "server.port" + const n = 1 + 1 + 1 + 1 + 1 + s := make([]attribute.KeyValue, 0, n) + return &s + }, + } + addOpPool = &sync.Pool{ + New: func() any { + const n = 1 // WithAttributeSet + o := make([]metric.AddOption, 0, n) + return &o + }, + } + recordOptPool = &sync.Pool{ + New: func() any { + const n = 1 + 1 // WithAttributeSet + "rpc.grpc.status_code" + o := make([]metric.RecordOption, 0, n) + return &o + }, + } +) + +func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) } +func put[T any](p *sync.Pool, s *[]T) { + *s = (*s)[:0] + p.Put(s) } -type ExporterMetrics struct { +// Instrumentation is experimental instrumentation for the exporter. +type Instrumentation struct { logInflightMetric otelconv.SDKExporterLogInflight logExportedMetric otelconv.SDKExporterLogExported logExportedDurationMetric otelconv.SDKExporterOperationDuration presetAttrs []attribute.KeyValue + setOpt metric.MeasurementOption } -func NewExporterMetrics( +// NewInstrumentation returns instrumentation for otlplog grpc exporter. +func NewInstrumentation( name, componentName string, componentType otelconv.ComponentTypeAttr, target string, -) (*ExporterMetrics, error) { - em := &ExporterMetrics{} +) (*Instrumentation, error) { + em := &Instrumentation{} mp := otel.GetMeterProvider() m := mp.Meter( @@ -82,39 +107,74 @@ func NewExporterMetrics( semconv.OTelComponentTypeKey.String(string(componentType)), } em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) + s := attribute.NewSet(em.presetAttrs...) + em.setOpt = metric.WithAttributeSet(s) + return em, nil } -func (em *ExporterMetrics) TrackExport( - ctx context.Context, - count int64, -) func(err error, successCount int64, code codes.Code) { - attrs := attrsPool.Get().(*[]attribute.KeyValue) - *attrs = append([]attribute.KeyValue{}, em.presetAttrs...) - - begin := time.Now() - em.logInflightMetric.AddSet(ctx, count, attribute.NewSet(*attrs...)) - return func(err error, successCount int64, code codes.Code) { - defer func() { - *attrs = (*attrs)[:0] - attrsPool.Put(attrs) - }() - - duration := time.Since(begin).Seconds() - em.logInflightMetric.AddSet(ctx, -count, attribute.NewSet(*attrs...)) - em.logExportedMetric.AddSet(ctx, successCount, attribute.NewSet(*attrs...)) +// ExportSpanDone is a function that is called when a call to an Exporter's +// ExportSpans method completes +// +// The number of successful exports is provided as success. Any error that is encountered is provided as error +// The code of last gRPC requests performed in scope of this export call. +type ExportSpanDone func(err error, success int64, code codes.Code) + +// ExportSpans instruments the ExportSpans method of the exporter. It returns a +// function that needs to be deferred so it is called when the method returns. +func (i *Instrumentation) ExportSpans(ctx context.Context, count int64) ExportSpanDone { + addOpt := get[metric.AddOption](addOpPool) + defer put(addOpPool, addOpt) + + *addOpt = append(*addOpt, i.setOpt) + + start := time.Now() + i.logInflightMetric.Inst().Add(ctx, count, *addOpt...) + + return i.end(ctx, start, count) +} + +func (i *Instrumentation) end(ctx context.Context, start time.Time, count int64) ExportSpanDone { + return func(err error, success int64, code codes.Code) { + addOpt := get[metric.AddOption](addOpPool) + defer put(addOpPool, addOpt) + + *addOpt = append(*addOpt, i.setOpt) + + duration := time.Since(start).Seconds() + i.logInflightMetric.Inst().Add(ctx, -count, *addOpt...) + i.logExportedMetric.Inst().Add(ctx, success, *addOpt...) + + mOpt := i.setOpt if err != nil { + attrs := get[attribute.KeyValue](attrsPool) + defer put(attrsPool, attrs) + *attrs = append(*attrs, i.presetAttrs...) *attrs = append(*attrs, semconv.ErrorType(err)) - em.logExportedMetric.AddSet(ctx, count-successCount, attribute.NewSet(*attrs...)) + + set := attribute.NewSet(*attrs...) + mOpt = metric.WithAttributeSet(set) + + *addOpt = append((*addOpt)[:0], mOpt) + + i.logExportedMetric.Inst().Add(ctx, count-success, *addOpt...) } - *attrs = append( - *attrs, - em.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), + + recordOpt := get[metric.RecordOption](recordOptPool) + defer put(recordOptPool, recordOpt) + *recordOpt = append( + *recordOpt, + mOpt, + metric.WithAttributes( + i.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), + ), ) - em.logExportedDurationMetric.RecordSet(ctx, duration, attribute.NewSet(*attrs...)) + i.logExportedDurationMetric.Inst().Record(ctx, duration, *recordOpt...) } } +// ServerAddrAttrs is a function that extracts server address and port attributes +// from a target string. func ServerAddrAttrs(target string) []attribute.KeyValue { if !strings.Contains(target, "://") { return splitHostPortAttrs(target) diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation_test.go similarity index 98% rename from exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go rename to exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation_test.go index fd3abe627c5..82dbb52c9c8 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/selfobservability_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation_test.go @@ -45,7 +45,7 @@ func (m *errMeter) Float64Histogram(string, ...mapi.Float64HistogramOption) (map func TestNewExporterMetrics(t *testing.T) { t.Run("No Error", func(t *testing.T) { - em, err := NewExporterMetrics( + em, err := NewInstrumentation( "newExportMetricsTest", "newExportMetricsTest/1", "newExportMetricsTest", @@ -70,7 +70,7 @@ func TestNewExporterMetrics(t *testing.T) { mp := &errMeterProvider{err: assert.AnError} otel.SetMeterProvider(mp) - _, err := NewExporterMetrics( + _, err := NewInstrumentation( "newExportMetrics", "newExportMetrics/1", "newExportMetrics", From 16eaf3c088d0fb62b318b1e648eae46da70b75cc Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 10 Sep 2025 23:02:16 +0800 Subject: [PATCH 41/42] fix the changlog check --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abc2c7786d..e679cec50a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `WithInstrumentationAttributeSet` option to `go.opentelemetry.io/otel/log`, `go.opentelemetry.io/otel/metric`, and `go.opentelemetry.io/otel/trace` packages. This provides a concurrent-safe and performant alternative to `WithInstrumentationAttributes` by accepting a pre-constructed `attribute.Set`. (#7287) +- - Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. +Check the `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x` package documentation for more information. (#7084) ### Fixed @@ -83,8 +85,6 @@ The next release will require at least [Go 1.24]. - The `go.opentelemetry.io/otel/semconv/v1.37.0` package. The package contains semantic conventions from the `v1.37.0` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/v1.37.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.36.0.`(#7254) -- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. - Check the `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x` package documentation for more information. (#7084) ### Changed From 74033bc65fea32b4a4ff2ccd1acd29d030dbe15f Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 10 Sep 2025 23:27:56 +0800 Subject: [PATCH 42/42] refactor: rename the selfoberv to observ and direct use the inst --- exporters/otlp/otlplog/otlploggrpc/client.go | 10 ++-- .../otlp/otlplog/otlploggrpc/client_test.go | 10 ++-- .../instrumentation.go | 52 +++++++++++-------- .../instrumentation_test.go | 2 +- 4 files changed, 41 insertions(+), 33 deletions(-) rename exporters/otlp/otlplog/otlploggrpc/internal/{selfobservability => observ}/instrumentation.go (79%) rename exporters/otlp/otlplog/otlploggrpc/internal/{selfobservability => observ}/instrumentation_test.go (96%) diff --git a/exporters/otlp/otlplog/otlploggrpc/client.go b/exporters/otlp/otlplog/otlploggrpc/client.go index 20fdb87fc30..0bbbec912ad 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client.go +++ b/exporters/otlp/otlplog/otlploggrpc/client.go @@ -23,8 +23,8 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/counter" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/observ" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/retry" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/x" "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" ) @@ -43,7 +43,7 @@ type client struct { conn *grpc.ClientConn lsc collogpb.LogsServiceClient - exporterMetric *selfobservability.Instrumentation + instrumentation *observ.Instrumentation } // Used for testing. @@ -85,7 +85,7 @@ func newClient(cfg config) (*client, error) { id := counter.NextExporterID() componentName := fmt.Sprintf("%s/%d", otelconv.ComponentTypeOtlpGRPCLogExporter, id) var err error - c.exporterMetric, err = selfobservability.NewInstrumentation( + c.instrumentation, err = observ.NewInstrumentation( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", componentName, otelconv.ComponentTypeOtlpGRPCLogExporter, @@ -154,9 +154,9 @@ func (c *client) UploadLogs(ctx context.Context, rl []*logpb.ResourceLogs) (err success := int64(len(rl)) // partialSuccessErr records an error when the export is partially successful. var partialSuccessErr error - if c.exporterMetric != nil { + if c.instrumentation != nil { count := len(rl) - trackExportFunc := c.exporterMetric.ExportSpans(ctx, int64(count)) + trackExportFunc := c.instrumentation.ExportSpans(ctx, int64(count)) defer func() { if partialSuccessErr != nil { trackExportFunc(partialSuccessErr, success, status.Code(partialSuccessErr)) diff --git a/exporters/otlp/otlplog/otlploggrpc/client_test.go b/exporters/otlp/otlplog/otlploggrpc/client_test.go index 413e909ef3c..b3f3520eb08 100644 --- a/exporters/otlp/otlplog/otlploggrpc/client_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/client_test.go @@ -30,7 +30,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/observ" "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/log" @@ -621,7 +621,7 @@ func TestSelfObservability(t *testing.T) { enabled: false, test: func(t *testing.T, _ func() metricdata.ScopeMetrics) { client, _ := clientFactory(t, nil) - assert.Empty(t, client.exporterMetric) + assert.Empty(t, client.instrumentation) }, }, { @@ -635,7 +635,7 @@ func TestSelfObservability(t *testing.T) { otelconv.ComponentTypeOtlpGRPCLogExporter, 0, ) - serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) + serverAddrAttrs := observ.ServerAddrAttrs(client.conn.Target()) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", @@ -757,7 +757,7 @@ func TestSelfObservability(t *testing.T) { otelconv.ComponentTypeOtlpGRPCLogExporter, 1, ) - serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) + serverAddrAttrs := observ.ServerAddrAttrs(client.conn.Target()) wantErr := fmt.Errorf("OTLP partial success: %s (%d log records rejected)", msg, n) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ @@ -898,7 +898,7 @@ func TestSelfObservability(t *testing.T) { otelconv.ComponentTypeOtlpGRPCLogExporter, 2, ) - serverAddrAttrs := selfobservability.ServerAddrAttrs(client.conn.Target()) + serverAddrAttrs := observ.ServerAddrAttrs(client.conn.Target()) wantMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc", diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation.go b/exporters/otlp/otlplog/otlploggrpc/internal/observ/instrumentation.go similarity index 79% rename from exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation.go rename to exporters/otlp/otlplog/otlploggrpc/internal/observ/instrumentation.go index 5fa055057f3..12ad406ba57 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/observ/instrumentation.go @@ -1,9 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// Package selfobservability provides self-observability metrics for OTLP log exporters. +// Package observ provides self-observability metrics for OTLP log exporters. // This is an experimental feature controlled by the x.SelfObservability feature flag. -package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" +package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/observ" import ( "context" @@ -59,9 +59,9 @@ func put[T any](p *sync.Pool, s *[]T) { // Instrumentation is experimental instrumentation for the exporter. type Instrumentation struct { - logInflightMetric otelconv.SDKExporterLogInflight - logExportedMetric otelconv.SDKExporterLogExported - logExportedDurationMetric otelconv.SDKExporterOperationDuration + logInflightMetric metric.Int64UpDownCounter + logExportedMetric metric.Int64Counter + logExportedDurationMetric metric.Float64Histogram presetAttrs []attribute.KeyValue setOpt metric.MeasurementOption } @@ -72,7 +72,7 @@ func NewInstrumentation( componentType otelconv.ComponentTypeAttr, target string, ) (*Instrumentation, error) { - em := &Instrumentation{} + i := &Instrumentation{} mp := otel.GetMeterProvider() m := mp.Meter( @@ -81,36 +81,44 @@ func NewInstrumentation( metric.WithSchemaURL(semconv.SchemaURL), ) - var err, e error - if em.logInflightMetric, e = otelconv.NewSDKExporterLogInflight(m); e != nil { + var err error + + logInflightMetric, e := otelconv.NewSDKExporterLogInflight(m) + if e != nil { e = fmt.Errorf("failed to create span inflight metric: %w", e) otel.Handle(e) err = errors.Join(err, e) } - if em.logExportedMetric, e = otelconv.NewSDKExporterLogExported(m); e != nil { + i.logInflightMetric = logInflightMetric.Inst() + + logExportedMetric, e := otelconv.NewSDKExporterLogExported(m) + if e != nil { e = fmt.Errorf("failed to create span exported metric: %w", e) otel.Handle(e) err = errors.Join(err, e) } - if em.logExportedDurationMetric, e = otelconv.NewSDKExporterOperationDuration(m); e != nil { + i.logExportedMetric = logExportedMetric.Inst() + + logOpDurationMetric, e := otelconv.NewSDKExporterOperationDuration(m) + if e != nil { e = fmt.Errorf("failed to create operation duration metric: %w", e) otel.Handle(e) err = errors.Join(err, e) } - + i.logExportedDurationMetric = logOpDurationMetric.Inst() if err != nil { return nil, err } - em.presetAttrs = []attribute.KeyValue{ + i.presetAttrs = []attribute.KeyValue{ semconv.OTelComponentName(componentName), semconv.OTelComponentTypeKey.String(string(componentType)), } - em.presetAttrs = append(em.presetAttrs, ServerAddrAttrs(target)...) - s := attribute.NewSet(em.presetAttrs...) - em.setOpt = metric.WithAttributeSet(s) + i.presetAttrs = append(i.presetAttrs, ServerAddrAttrs(target)...) + s := attribute.NewSet(i.presetAttrs...) + i.setOpt = metric.WithAttributeSet(s) - return em, nil + return i, nil } // ExportSpanDone is a function that is called when a call to an Exporter's @@ -129,7 +137,7 @@ func (i *Instrumentation) ExportSpans(ctx context.Context, count int64) ExportSp *addOpt = append(*addOpt, i.setOpt) start := time.Now() - i.logInflightMetric.Inst().Add(ctx, count, *addOpt...) + i.logInflightMetric.Add(ctx, count, *addOpt...) return i.end(ctx, start, count) } @@ -142,8 +150,8 @@ func (i *Instrumentation) end(ctx context.Context, start time.Time, count int64) *addOpt = append(*addOpt, i.setOpt) duration := time.Since(start).Seconds() - i.logInflightMetric.Inst().Add(ctx, -count, *addOpt...) - i.logExportedMetric.Inst().Add(ctx, success, *addOpt...) + i.logInflightMetric.Add(ctx, -count, *addOpt...) + i.logExportedMetric.Add(ctx, success, *addOpt...) mOpt := i.setOpt if err != nil { @@ -157,7 +165,7 @@ func (i *Instrumentation) end(ctx context.Context, start time.Time, count int64) *addOpt = append((*addOpt)[:0], mOpt) - i.logExportedMetric.Inst().Add(ctx, count-success, *addOpt...) + i.logExportedMetric.Add(ctx, count-success, *addOpt...) } recordOpt := get[metric.RecordOption](recordOptPool) @@ -166,10 +174,10 @@ func (i *Instrumentation) end(ctx context.Context, start time.Time, count int64) *recordOpt, mOpt, metric.WithAttributes( - i.logExportedDurationMetric.AttrRPCGRPCStatusCode(otelconv.RPCGRPCStatusCodeAttr(code)), + semconv.RPCGRPCStatusCodeKey.Int64(int64(code)), ), ) - i.logExportedDurationMetric.Inst().Record(ctx, duration, *recordOpt...) + i.logExportedDurationMetric.Record(ctx, duration, *recordOpt...) } } diff --git a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation_test.go b/exporters/otlp/otlplog/otlploggrpc/internal/observ/instrumentation_test.go similarity index 96% rename from exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation_test.go rename to exporters/otlp/otlplog/otlploggrpc/internal/observ/instrumentation_test.go index 82dbb52c9c8..35d1de80fae 100644 --- a/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability/instrumentation_test.go +++ b/exporters/otlp/otlplog/otlploggrpc/internal/observ/instrumentation_test.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/selfobservability" +package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc/internal/observ" import ( "testing"