Skip to content

Commit aed2fac

Browse files
committed
Merge branch 'main' into prop-ctx-sdk-trace-obs
2 parents 819979f + 44a0d62 commit aed2fac

File tree

3 files changed

+215
-11
lines changed

3 files changed

+215
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ The next release will require at least [Go 1.24].
7272

7373
- Fix `go.opentelemetry.io/otel/exporters/prometheus` to deduplicate suffixes if already present in metric name when UTF8 is enabled. (#7088)
7474
- Fix the `go.opentelemetry.io/otel/exporters/stdout/stdouttrace` self-observability component type and name. (#7195)
75+
- Fix partial export count metric in `go.opentelemetry.io/otel/exporters/stdout/stdouttrace`. (#7199)
7576
- Propagate context to self-observability measurements in `go.opentelemetry.io/otel/sdk/trace`. (#7209)
7677

7778
<!-- Released section -->

exporters/stdout/stdouttrace/trace.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,24 +96,50 @@ type Exporter struct {
9696
operationDurationMetric otelconv.SDKExporterOperationDuration
9797
}
9898

99+
var measureAttrsPool = sync.Pool{
100+
New: func() any {
101+
// "component.name" + "component.type" + "error.type"
102+
const n = 1 + 1 + 1
103+
s := make([]attribute.KeyValue, 0, n)
104+
// Return a pointer to a slice instead of a slice itself
105+
// to avoid allocations on every call.
106+
return &s
107+
},
108+
}
109+
99110
// ExportSpans writes spans in json format to stdout.
100111
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) (err error) {
112+
var success int64
101113
if e.selfObservabilityEnabled {
102114
count := int64(len(spans))
103115

104116
e.spanInflightMetric.Add(ctx, count, e.selfObservabilityAttrs...)
105117
defer func(starting time.Time) {
106-
// additional attributes for self-observability,
107-
// only spanExportedMetric and operationDurationMetric are supported
108-
addAttrs := make([]attribute.KeyValue, len(e.selfObservabilityAttrs), len(e.selfObservabilityAttrs)+1)
109-
copy(addAttrs, e.selfObservabilityAttrs)
118+
e.spanInflightMetric.Add(ctx, -count, e.selfObservabilityAttrs...)
119+
120+
// Record the success and duration of the operation.
121+
//
122+
// Do not exclude 0 values, as they are valid and indicate no spans
123+
// were exported which is meaningful for certain aggregations.
124+
e.spanExportedMetric.Add(ctx, success, e.selfObservabilityAttrs...)
125+
126+
attr := e.selfObservabilityAttrs
110127
if err != nil {
111-
addAttrs = append(addAttrs, semconv.ErrorType(err))
128+
// additional attributes for self-observability,
129+
// only spanExportedMetric and operationDurationMetric are supported.
130+
attrs := measureAttrsPool.Get().(*[]attribute.KeyValue)
131+
defer func() {
132+
*attrs = (*attrs)[:0] // reset the slice for reuse
133+
measureAttrsPool.Put(attrs)
134+
}()
135+
*attrs = append(*attrs, e.selfObservabilityAttrs...)
136+
*attrs = append(*attrs, semconv.ErrorType(err))
137+
attr = *attrs
138+
139+
e.spanExportedMetric.Add(ctx, count-success, attr...)
112140
}
113141

114-
e.spanInflightMetric.Add(ctx, -count, e.selfObservabilityAttrs...)
115-
e.spanExportedMetric.Add(ctx, count, addAttrs...)
116-
e.operationDurationMetric.Record(ctx, time.Since(starting).Seconds(), addAttrs...)
142+
e.operationDurationMetric.Record(ctx, time.Since(starting).Seconds(), attr...)
117143
}(time.Now())
118144
}
119145

@@ -148,11 +174,13 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
148174
}
149175

150176
// Encode span stubs, one by one
151-
if err := e.encoder.Encode(stub); err != nil {
152-
return err
177+
if e := e.encoder.Encode(stub); e != nil {
178+
err = errors.Join(err, fmt.Errorf("failed to encode span %d: %w", i, e))
179+
continue
153180
}
181+
success++
154182
}
155-
return nil
183+
return err
156184
}
157185

158186
// Shutdown is called to stop the exporter, it performs no action.

exporters/stdout/stdouttrace/trace_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"encoding/json"
1010
"io"
11+
"math"
1112
"testing"
1213
"time"
1314

@@ -402,6 +403,17 @@ func TestSelfObservability(t *testing.T) {
402403
Temporality: metricdata.CumulativeTemporality,
403404
IsMonotonic: true,
404405
DataPoints: []metricdata.DataPoint[int64]{
406+
{
407+
Attributes: attribute.NewSet(
408+
semconv.OTelComponentName(
409+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
410+
),
411+
semconv.OTelComponentTypeKey.String(
412+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
413+
),
414+
),
415+
Value: 0,
416+
},
405417
{
406418
Attributes: attribute.NewSet(
407419
semconv.OTelComponentName(
@@ -441,6 +453,146 @@ func TestSelfObservability(t *testing.T) {
441453
}, sm.Metrics[2], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
442454
},
443455
},
456+
{
457+
name: "PartialExport",
458+
enabled: true,
459+
callExportSpans: func(t *testing.T, exporter *stdouttrace.Exporter) {
460+
t.Helper()
461+
462+
err := exporter.ExportSpans(context.Background(), tracetest.SpanStubs{
463+
{Name: "/foo"},
464+
{
465+
Name: "JSON encoder cannot marshal math.Inf(1)",
466+
Attributes: []attribute.KeyValue{attribute.Float64("", math.Inf(1))},
467+
},
468+
{Name: "/bar"},
469+
}.Snapshots())
470+
require.Error(t, err)
471+
},
472+
assertMetrics: func(t *testing.T, rm metricdata.ResourceMetrics) {
473+
t.Helper()
474+
require.Len(t, rm.ScopeMetrics, 1)
475+
476+
sm := rm.ScopeMetrics[0]
477+
require.Len(t, sm.Metrics, 3)
478+
479+
assert.Equal(t, instrumentation.Scope{
480+
Name: "go.opentelemetry.io/otel/exporters/stdout/stdouttrace",
481+
Version: sdk.Version(),
482+
SchemaURL: semconv.SchemaURL,
483+
}, sm.Scope)
484+
485+
metricdatatest.AssertEqual(t, metricdata.Metrics{
486+
Name: otelconv.SDKExporterSpanInflight{}.Name(),
487+
Description: otelconv.SDKExporterSpanInflight{}.Description(),
488+
Unit: otelconv.SDKExporterSpanInflight{}.Unit(),
489+
Data: metricdata.Sum[int64]{
490+
Temporality: metricdata.CumulativeTemporality,
491+
DataPoints: []metricdata.DataPoint[int64]{
492+
{
493+
Attributes: attribute.NewSet(
494+
semconv.OTelComponentName(
495+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
496+
),
497+
semconv.OTelComponentTypeKey.String(
498+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
499+
),
500+
),
501+
Value: 0,
502+
},
503+
},
504+
},
505+
}, sm.Metrics[0], metricdatatest.IgnoreTimestamp())
506+
507+
require.IsType(t, metricdata.Sum[int64]{}, sm.Metrics[1].Data)
508+
sum := sm.Metrics[1].Data.(metricdata.Sum[int64])
509+
var found bool
510+
for i := range sum.DataPoints {
511+
sum.DataPoints[i].Attributes, _ = sum.DataPoints[i].Attributes.Filter(
512+
func(kv attribute.KeyValue) bool {
513+
if kv.Key == semconv.ErrorTypeKey {
514+
found = true
515+
return false
516+
}
517+
return true
518+
},
519+
)
520+
}
521+
assert.True(t, found, "missing error type attribute in span export metric")
522+
sm.Metrics[1].Data = sum
523+
524+
metricdatatest.AssertEqual(t, metricdata.Metrics{
525+
Name: otelconv.SDKExporterSpanExported{}.Name(),
526+
Description: otelconv.SDKExporterSpanExported{}.Description(),
527+
Unit: otelconv.SDKExporterSpanExported{}.Unit(),
528+
Data: metricdata.Sum[int64]{
529+
Temporality: metricdata.CumulativeTemporality,
530+
IsMonotonic: true,
531+
DataPoints: []metricdata.DataPoint[int64]{
532+
{
533+
Attributes: attribute.NewSet(
534+
semconv.OTelComponentName(
535+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
536+
),
537+
semconv.OTelComponentTypeKey.String(
538+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
539+
),
540+
),
541+
Value: 1,
542+
},
543+
{
544+
Attributes: attribute.NewSet(
545+
semconv.OTelComponentName(
546+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
547+
),
548+
semconv.OTelComponentTypeKey.String(
549+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
550+
),
551+
),
552+
Value: 2,
553+
},
554+
},
555+
},
556+
}, sm.Metrics[1], metricdatatest.IgnoreTimestamp())
557+
558+
require.IsType(t, metricdata.Histogram[float64]{}, sm.Metrics[2].Data)
559+
hist := sm.Metrics[2].Data.(metricdata.Histogram[float64])
560+
require.Len(t, hist.DataPoints, 1)
561+
found = false
562+
hist.DataPoints[0].Attributes, _ = hist.DataPoints[0].Attributes.Filter(
563+
func(kv attribute.KeyValue) bool {
564+
if kv.Key == semconv.ErrorTypeKey {
565+
found = true
566+
return false
567+
}
568+
return true
569+
},
570+
)
571+
assert.True(t, found, "missing error type attribute in operation duration metric")
572+
sm.Metrics[2].Data = hist
573+
574+
metricdatatest.AssertEqual(t, metricdata.Metrics{
575+
Name: otelconv.SDKExporterOperationDuration{}.Name(),
576+
Description: otelconv.SDKExporterOperationDuration{}.Description(),
577+
Unit: otelconv.SDKExporterOperationDuration{}.Unit(),
578+
Data: metricdata.Histogram[float64]{
579+
Temporality: metricdata.CumulativeTemporality,
580+
DataPoints: []metricdata.HistogramDataPoint[float64]{
581+
{
582+
Attributes: attribute.NewSet(
583+
semconv.OTelComponentName(
584+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter/0",
585+
),
586+
semconv.OTelComponentTypeKey.String(
587+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace.Exporter",
588+
),
589+
),
590+
},
591+
},
592+
},
593+
}, sm.Metrics[2], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
594+
},
595+
},
444596
}
445597

446598
for _, tt := range tests {
@@ -515,3 +667,26 @@ func TestSelfObservabilityInstrumentErrors(t *testing.T) {
515667
assert.ErrorContains(t, err, "span exported metric")
516668
assert.ErrorContains(t, err, "operation duration metric")
517669
}
670+
671+
func BenchmarkExporterExportSpans(b *testing.B) {
672+
b.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true")
673+
ss := tracetest.SpanStubs{
674+
{Name: "/foo"},
675+
{
676+
Name: "JSON encoder cannot marshal math.Inf(1)",
677+
Attributes: []attribute.KeyValue{attribute.Float64("", math.Inf(1))},
678+
},
679+
{Name: "/bar"},
680+
}.Snapshots()
681+
ex, err := stdouttrace.New(stdouttrace.WithWriter(io.Discard))
682+
if err != nil {
683+
b.Fatalf("failed to create exporter: %v", err)
684+
}
685+
686+
b.ReportAllocs()
687+
b.ResetTimer()
688+
for i := 0; i < b.N; i++ {
689+
err = ex.ExportSpans(context.Background(), ss)
690+
}
691+
_ = err
692+
}

0 commit comments

Comments
 (0)