1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-09-16 09:26:25 +02:00

Handle partial export counts in stdouttrace observability (#7199)

Do not fail all exported metric counts if only some of them failed.
This commit is contained in:
Tyler Yahn
2025-08-18 09:37:33 -07:00
committed by GitHub
parent 75db61c29e
commit 907d93b8e1
3 changed files with 178 additions and 11 deletions

View File

@@ -72,6 +72,7 @@ The next release will require at least [Go 1.24].
- Fix `go.opentelemetry.io/otel/exporters/prometheus` to deduplicate suffixes if already present in metric name when UTF8 is enabled. (#7088)
- Fix the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace` self-observability component type and name. (#7195)
- Fix partial export count metric in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`. (#7199)
<!-- Released section -->
<!-- Don't change this section unless doing release -->

View File

@@ -98,22 +98,34 @@ type Exporter struct {
// ExportSpans writes spans in json format to stdout.
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) (err error) {
var success int64
if e.selfObservabilityEnabled {
count := int64(len(spans))
e.spanInflightMetric.Add(ctx, count, e.selfObservabilityAttrs...)
defer func(starting time.Time) {
// additional attributes for self-observability,
// only spanExportedMetric and operationDurationMetric are supported
addAttrs := make([]attribute.KeyValue, len(e.selfObservabilityAttrs), len(e.selfObservabilityAttrs)+1)
copy(addAttrs, e.selfObservabilityAttrs)
e.spanInflightMetric.Add(ctx, -count, e.selfObservabilityAttrs...)
// Record the success and duration of the operation.
//
// Do not exclude 0 values, as they are valid and indicate no spans
// were exported which is meaningful for certain aggregations.
e.spanExportedMetric.Add(ctx, success, e.selfObservabilityAttrs...)
attr := e.selfObservabilityAttrs
if err != nil {
addAttrs = append(addAttrs, semconv.ErrorType(err))
// additional attributes for self-observability,
// only spanExportedMetric and operationDurationMetric are supported.
//
// TODO: use a pool to amortize allocations.
attr = make([]attribute.KeyValue, len(e.selfObservabilityAttrs), len(e.selfObservabilityAttrs)+1)
copy(attr, e.selfObservabilityAttrs)
attr = append(attr, semconv.ErrorType(err))
e.spanExportedMetric.Add(ctx, count-success, attr...)
}
e.spanInflightMetric.Add(ctx, -count, e.selfObservabilityAttrs...)
e.spanExportedMetric.Add(ctx, count, addAttrs...)
e.operationDurationMetric.Record(ctx, time.Since(starting).Seconds(), addAttrs...)
e.operationDurationMetric.Record(ctx, time.Since(starting).Seconds(), attr...)
}(time.Now())
}
@@ -148,11 +160,13 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
}
// Encode span stubs, one by one
if err := e.encoder.Encode(stub); err != nil {
if e := e.encoder.Encode(stub); e != nil {
err = errors.Join(err, fmt.Errorf("failed to encode span %d: %w", i, e))
continue
}
success++
}
return err
}
}
return nil
}
// Shutdown is called to stop the exporter, it performs no action.

View File

@@ -8,6 +8,7 @@ import (
"context"
"encoding/json"
"io"
"math"
"testing"
"time"
@@ -402,6 +403,17 @@ func TestSelfObservability(t *testing.T) {
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{
Attributes: attribute.NewSet(
semconv.OTelComponentName(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
),
semconv.OTelComponentTypeKey.String(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
),
),
Value: 0,
},
{
Attributes: attribute.NewSet(
semconv.OTelComponentName(
@@ -441,6 +453,146 @@ func TestSelfObservability(t *testing.T) {
}, sm.Metrics[2], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
},
},
{
name: "PartialExport",
enabled: true,
callExportSpans: func(t *testing.T, exporter *stdouttrace.Exporter) {
t.Helper()
err := exporter.ExportSpans(context.Background(), tracetest.SpanStubs{
{Name: "/foo"},
{
Name: "JSON encoder cannot marshal math.Inf(1)",
Attributes: []attribute.KeyValue{attribute.Float64("", math.Inf(1))},
},
{Name: "/bar"},
}.Snapshots())
require.Error(t, err)
},
assertMetrics: func(t *testing.T, rm metricdata.ResourceMetrics) {
t.Helper()
require.Len(t, rm.ScopeMetrics, 1)
sm := rm.ScopeMetrics[0]
require.Len(t, sm.Metrics, 3)
assert.Equal(t, instrumentation.Scope{
Name: "go.opentelemetry.io/otel/exporters/stdout/stdouttrace",
Version: sdk.Version(),
SchemaURL: semconv.SchemaURL,
}, sm.Scope)
metricdatatest.AssertEqual(t, metricdata.Metrics{
Name: otelconv.SDKExporterSpanInflight{}.Name(),
Description: otelconv.SDKExporterSpanInflight{}.Description(),
Unit: otelconv.SDKExporterSpanInflight{}.Unit(),
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.DataPoint[int64]{
{
Attributes: attribute.NewSet(
semconv.OTelComponentName(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
),
semconv.OTelComponentTypeKey.String(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
),
),
Value: 0,
},
},
},
}, sm.Metrics[0], metricdatatest.IgnoreTimestamp())
require.IsType(t, metricdata.Sum[int64]{}, sm.Metrics[1].Data)
sum := sm.Metrics[1].Data.(metricdata.Sum[int64])
var found bool
for i := range sum.DataPoints {
sum.DataPoints[i].Attributes, _ = sum.DataPoints[i].Attributes.Filter(
func(kv attribute.KeyValue) bool {
if kv.Key == semconv.ErrorTypeKey {
found = true
return false
}
return true
},
)
}
assert.True(t, found, "missing error type attribute in span export metric")
sm.Metrics[1].Data = sum
metricdatatest.AssertEqual(t, metricdata.Metrics{
Name: otelconv.SDKExporterSpanExported{}.Name(),
Description: otelconv.SDKExporterSpanExported{}.Description(),
Unit: otelconv.SDKExporterSpanExported{}.Unit(),
Data: metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{
{
Attributes: attribute.NewSet(
semconv.OTelComponentName(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
),
semconv.OTelComponentTypeKey.String(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
),
),
Value: 1,
},
{
Attributes: attribute.NewSet(
semconv.OTelComponentName(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
),
semconv.OTelComponentTypeKey.String(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
),
),
Value: 2,
},
},
},
}, sm.Metrics[1], metricdatatest.IgnoreTimestamp())
require.IsType(t, metricdata.Histogram[float64]{}, sm.Metrics[2].Data)
hist := sm.Metrics[2].Data.(metricdata.Histogram[float64])
require.Len(t, hist.DataPoints, 1)
found = false
hist.DataPoints[0].Attributes, _ = hist.DataPoints[0].Attributes.Filter(
func(kv attribute.KeyValue) bool {
if kv.Key == semconv.ErrorTypeKey {
found = true
return false
}
return true
},
)
assert.True(t, found, "missing error type attribute in operation duration metric")
sm.Metrics[2].Data = hist
metricdatatest.AssertEqual(t, metricdata.Metrics{
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(
semconv.OTelComponentName(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
),
semconv.OTelComponentTypeKey.String(
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
),
),
},
},
},
}, sm.Metrics[2], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
},
},
}
for _, tt := range tests {