You've already forked opentelemetry-go
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:
@@ -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 -->
|
||||
|
@@ -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 {
|
||||
return err
|
||||
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 nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown is called to stop the exporter, it performs no action.
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user