1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-19 21:45:50 +02:00

fix(stdouttrace/observ): record error.type on opDuration when exportedSpans is disabled (#8432)

When `ExportOp.End` is called with a non-nil error and `exportedSpans`
metric is disabled but `opDuration` is enabled, the `error.type`
attribute is not included in the duration histogram record.

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
ian
2026-06-09 23:07:49 +08:00
committed by GitHub
parent 75e06c2343
commit 1802033e48
3 changed files with 102 additions and 5 deletions
+1
View File
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Fixed
- Interpret HTTP `Retry-After` header values as seconds instead of nanoseconds when retrying OTLP HTTP exports in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`, `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`, `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8383)
- Fix `go.opentelemetry.io/otel/exporters/stdout/stdouttrace` self-observability to record `error.type` on operation duration histogram when the `exportedSpans` metric is disabled. (#8432)
<!-- Released section -->
<!-- Don't change this section unless doing release -->
@@ -205,7 +205,7 @@ func (e ExportOp) End(success int64, err error) {
}
mOpt := e.inst.setOpt
if err != nil && exportedSpansEnable {
if err != nil && (exportedSpansEnable || opDurationEnable) {
attrs := get[attribute.KeyValue](measureAttrsPool)
defer put(measureAttrsPool, attrs)
*attrs = append(*attrs, e.inst.attrs...)
@@ -216,10 +216,11 @@ func (e ExportOp) End(success int64, err error) {
set := attribute.NewSet(*attrs...)
mOpt = metric.WithAttributeSet(set)
// Reset addOpt with new attribute set.
*addOpt = append((*addOpt)[:0], mOpt)
e.inst.exportedSpans.Add(e.ctx, e.nSpans-success, *addOpt...)
if exportedSpansEnable {
// Reset addOpt with new attribute set.
*addOpt = append((*addOpt)[:0], mOpt)
e.inst.exportedSpans.Add(e.ctx, e.nSpans-success, *addOpt...)
}
}
if opDurationEnable {
@@ -215,6 +215,101 @@ func TestInstrumentationExportSpansPartialErrored(t *testing.T) {
assertMetrics(t, collect(), n, success, assert.AnError)
}
func TestExportSpansErrorTypeRecorded(t *testing.T) {
exportedSpansName := otelconv.SDKExporterSpanExported{}.Name()
opDurationName := otelconv.SDKExporterOperationDuration{}.Name()
inflightSpansName := otelconv.SDKExporterSpanInflight{}.Name()
tests := []struct {
name string
drop string
errorOnExported bool
errorOnOpDuration bool
}{
{
name: "drop exportedSpans: opDuration still records error.type",
drop: exportedSpansName,
errorOnOpDuration: true,
},
{
name: "drop opDuration: exportedSpans still records error.type",
drop: opDurationName,
errorOnExported: true,
},
{
name: "drop inflightSpans: error.type still on exported and opDuration",
drop: inflightSpansName,
errorOnExported: true,
errorOnOpDuration: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
original := otel.GetMeterProvider()
t.Cleanup(func() { otel.SetMeterProvider(original) })
r := metric.NewManualReader()
mp := metric.NewMeterProvider(
metric.WithReader(r),
metric.WithView(metric.NewView(
metric.Instrument{Name: tt.drop},
metric.Stream{Aggregation: metric.AggregationDrop{}},
)),
)
otel.SetMeterProvider(mp)
inst, err := observ.NewInstrumentation(ID)
require.NoError(t, err)
require.NotNil(t, inst)
const n = 10
inst.ExportSpans(t.Context(), n).End(0, assert.AnError)
var rm metricdata.ResourceMetrics
require.NoError(t, r.Collect(t.Context(), &rm))
require.Len(t, rm.ScopeMetrics, 1)
got := rm.ScopeMetrics[0]
assert.Equal(t, Scope, got.Scope, "unexpected scope")
o := metricdatatest.IgnoreTimestamp()
seen := make(map[string]bool, 2)
for _, m := range got.Metrics {
seen[m.Name] = true
switch m.Name {
case exportedSpansName:
var exportedErr error
if tt.errorOnExported {
exportedErr = assert.AnError
}
metricdatatest.AssertEqual(t, spanExported(0, n, exportedErr), m, o)
case opDurationName:
var opErr error
if tt.errorOnOpDuration {
opErr = assert.AnError
}
metricdatatest.AssertEqual(
t, operationDuration(opErr), m, o, metricdatatest.IgnoreValue(),
)
case inflightSpansName:
metricdatatest.AssertEqual(t, spanInflight(), m, o)
}
}
// Verify the dropped instrument is absent and the other two are present.
assert.False(t, seen[tt.drop], "expected %q to be absent", tt.drop)
for _, name := range []string{exportedSpansName, opDurationName, inflightSpansName} {
if name == tt.drop {
continue
}
assert.True(t, seen[name], "expected %q to be present", name)
}
})
}
}
func BenchmarkInstrumentationExportSpans(b *testing.B) {
setup := func(b *testing.B) *observ.Instrumentation {
b.Helper()