Skip to content

Commit ad9ec76

Browse files
Machyneobs-gh-mattcotter
authored andcommitted
[connector/spanmetrics] Use latest semantic conventions for status code attribute
1 parent 38f9da6 commit ad9ec76

File tree

4 files changed

+160
-9
lines changed

4 files changed

+160
-9
lines changed

.chloggen/mc_spanmetrics.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: spanmetricsconnector
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add a feature gate to use the latest semantic conventions for the status code attribute on generated metrics.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [42103]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

connector/spanmetricsconnector/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ across all spans:
5454
- `service.name`
5555
- `span.name`
5656
- `span.kind`
57-
- `status.code`
57+
- `status.code` (or `otel.status_code` when the `spanmetrics.statusCodeConvention.useOtelPrefix` feature gate is enabled)
5858
- `collector.instance.id`
5959

6060
The `collector.instance.id` dimension is intended to add a unique UUID to all metrics, ensuring that the spanmetrics connector

connector/spanmetricsconnector/connector.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/lightstep/go-expohisto/structure"
1515
"go.opentelemetry.io/collector/component"
1616
"go.opentelemetry.io/collector/consumer"
17+
"go.opentelemetry.io/collector/featuregate"
1718
"go.opentelemetry.io/collector/pdata/pcommon"
1819
"go.opentelemetry.io/collector/pdata/pmetric"
1920
"go.opentelemetry.io/collector/pdata/ptrace"
@@ -27,12 +28,19 @@ import (
2728
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil"
2829
)
2930

31+
var useOtelStatusCodeAttribute = featuregate.GlobalRegistry().MustRegister(
32+
"spanmetrics.statusCodeConvention.useOtelPrefix",
33+
featuregate.StageAlpha,
34+
featuregate.WithRegisterDescription("When enabled, generated metrics will use `otel.status_code=ERROR` instead of `status.code=STATUS_CODE_ERROR`"),
35+
)
36+
3037
const (
3138
serviceNameKey = string(conventions.ServiceNameKey)
3239
spanNameKey = "span.name" // OpenTelemetry non-standard constant.
3340
spanKindKey = "span.kind" // OpenTelemetry non-standard constant.
3441
statusCodeKey = "status.code" // OpenTelemetry non-standard constant.
3542
collectorInstanceKey = "collector.instance.id" // OpenTelemetry non-standard constant.
43+
otelStatusCodeKey = "otel.status_code" // OpenTelemetry non-standard constant.
3644
instrumentationScopeNameKey = "span.instrumentation.scope.name" // OpenTelemetry non-standard constant.
3745
instrumentationScopeVersionKey = "span.instrumentation.scope.version" // OpenTelemetry non-standard constant.
3846
metricKeySeparator = string(byte(0))
@@ -542,8 +550,18 @@ func (p *connectorImp) buildAttributes(
542550
if !contains(p.config.ExcludeDimensions, spanKindKey) {
543551
attr.PutStr(spanKindKey, traceutil.SpanKindStr(span.Kind()))
544552
}
545-
if !contains(p.config.ExcludeDimensions, statusCodeKey) {
546-
attr.PutStr(statusCodeKey, traceutil.StatusCodeStr(span.Status().Code()))
553+
if useOtelStatusCodeAttribute.IsEnabled() {
554+
if !contains(p.config.ExcludeDimensions, otelStatusCodeKey) {
555+
if span.Status().Code() == ptrace.StatusCodeError {
556+
attr.PutStr(otelStatusCodeKey, "ERROR")
557+
} else if span.Status().Code() == ptrace.StatusCodeOk {
558+
attr.PutStr(otelStatusCodeKey, "OK")
559+
}
560+
}
561+
} else {
562+
if !contains(p.config.ExcludeDimensions, statusCodeKey) {
563+
attr.PutStr(statusCodeKey, traceutil.StatusCodeStr(span.Status().Code()))
564+
}
547565
}
548566
if includeCollectorInstanceID.IsEnabled() {
549567
if !contains(p.config.ExcludeDimensions, collectorInstanceKey) {
@@ -595,8 +613,14 @@ func (p *connectorImp) buildKey(serviceName string, span ptrace.Span, optionalDi
595613
if !contains(p.config.ExcludeDimensions, spanKindKey) {
596614
concatDimensionValue(p.keyBuf, traceutil.SpanKindStr(span.Kind()), true)
597615
}
598-
if !contains(p.config.ExcludeDimensions, statusCodeKey) {
599-
concatDimensionValue(p.keyBuf, traceutil.StatusCodeStr(span.Status().Code()), true)
616+
if useOtelStatusCodeAttribute.IsEnabled() {
617+
if !contains(p.config.ExcludeDimensions, otelStatusCodeKey) {
618+
concatDimensionValue(p.keyBuf, traceutil.StatusCodeStr(span.Status().Code()), true)
619+
}
620+
} else {
621+
if !contains(p.config.ExcludeDimensions, statusCodeKey) {
622+
concatDimensionValue(p.keyBuf, traceutil.StatusCodeStr(span.Status().Code()), true)
623+
}
600624
}
601625

602626
for _, d := range optionalDims {

connector/spanmetricsconnector/connector_test.go

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ const (
5757

5858
// metricID represents the minimum attributes that uniquely identifies a metric in our tests.
5959
type metricID struct {
60-
service string
61-
name string
62-
kind string
63-
statusCode string
60+
service string
61+
name string
62+
kind string
63+
statusCode string
64+
otelStatusCode string
6465
}
6566

6667
type metricDataPoint interface {
@@ -133,6 +134,66 @@ func verifyBadMetricsOkay(testing.TB, pmetric.Metrics) bool {
133134
return true // Validating no exception
134135
}
135136

137+
func verifyOtelStatusCode(tb testing.TB, input pmetric.Metrics) bool {
138+
require.Equal(tb, 8, input.DataPointCount(),
139+
"Should be 4 for each of call count and latency split into three resource scopes defined by: "+
140+
"service-a: service-a (server kind) -> service-a (client kind), "+
141+
"service-b: service-b (server kind), and"+
142+
"service-c: service-c (server kind)",
143+
)
144+
145+
require.Equal(tb, 3, input.ResourceMetrics().Len())
146+
147+
for i := 0; i < input.ResourceMetrics().Len(); i++ {
148+
rm := input.ResourceMetrics().At(i)
149+
150+
var expectedStatusCode string
151+
val, ok := rm.Resource().Attributes().Get(serviceNameKey)
152+
require.True(tb, ok)
153+
serviceName := val.AsString()
154+
switch serviceName {
155+
case "service-a":
156+
expectedStatusCode = "OK"
157+
case "service-b":
158+
expectedStatusCode = "ERROR"
159+
case "service-c":
160+
expectedStatusCode = ""
161+
default:
162+
require.Fail(tb, "Unexpected service name: %s", serviceName)
163+
}
164+
165+
ilm := rm.ScopeMetrics()
166+
require.Equal(tb, 1, ilm.Len())
167+
assert.Equal(tb, "spanmetricsconnector", ilm.At(0).Scope().Name())
168+
169+
m := ilm.At(0).Metrics()
170+
require.Equal(tb, 2, m.Len(), "only sum and histogram metric types generated")
171+
172+
metric := m.At(0)
173+
// validate the sum metrics for simplicity
174+
assert.Equal(tb, pmetric.MetricTypeSum, metric.Type())
175+
seenMetricIDs := make(map[metricID]bool)
176+
177+
dataPoints := metric.Sum().DataPoints()
178+
for dpi := 0; dpi < dataPoints.Len(); dpi++ {
179+
dp := dataPoints.At(dpi)
180+
statusCode, ok := dp.Attributes().Get(otelStatusCodeKey)
181+
if expectedStatusCode == "" {
182+
require.False(tb, ok)
183+
} else {
184+
require.True(tb, ok)
185+
assert.Equal(tb, expectedStatusCode, statusCode.AsString())
186+
}
187+
verifyMetricLabels(tb, dp, seenMetricIDs)
188+
}
189+
for id := range seenMetricIDs {
190+
assert.Equal(tb, expectedStatusCode, id.otelStatusCode)
191+
assert.Equal(tb, "", id.statusCode)
192+
}
193+
}
194+
return true
195+
}
196+
136197
// verifyConsumeMetricsInputDelta expects one accumulation of metrics, and marked as delta
137198
func verifyConsumeMetricsInputDelta(tb testing.TB, input pmetric.Metrics) bool {
138199
return verifyConsumeMetricsInput(tb, input, pmetric.AggregationTemporalityDelta, 1)
@@ -304,6 +365,8 @@ func verifyMetricLabels(tb testing.TB, dp metricDataPoint, seenMetricIDs map[met
304365
mID.kind = v.Str()
305366
case statusCodeKey:
306367
mID.statusCode = v.Str()
368+
case otelStatusCodeKey:
369+
mID.otelStatusCode = v.Str()
307370
case notInSpanAttrName1:
308371
assert.Fail(tb, notInSpanAttrName1+" should not be in this metric")
309372
default:
@@ -374,6 +437,26 @@ func buildSampleTrace() ptrace.Traces {
374437
return traces
375438
}
376439

440+
// buildTraceWithUnsetStatusCode builds the following trace:
441+
//
442+
// service-c/ping (server)
443+
func appendTraceWithUnsetStatusCode(traces ptrace.Traces) ptrace.Traces {
444+
initServiceSpans(
445+
serviceSpans{
446+
serviceName: "service-c",
447+
spans: []span{
448+
{
449+
name: "/ping",
450+
kind: ptrace.SpanKindServer,
451+
statusCode: ptrace.StatusCodeUnset,
452+
traceID: [16]byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20},
453+
spanID: [8]byte{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28},
454+
},
455+
},
456+
}, traces.ResourceSpans().AppendEmpty())
457+
return traces
458+
}
459+
377460
func initServiceSpans(serviceSpans serviceSpans, spans ptrace.ResourceSpans) {
378461
if serviceSpans.serviceName != "" {
379462
spans.Resource().Attributes().PutStr(string(conventions.ServiceNameKey), serviceSpans.serviceName)
@@ -774,6 +857,7 @@ func TestConsumeTraces(t *testing.T) {
774857
exemplarConfig func() ExemplarsConfig
775858
verifier func(tb testing.TB, input pmetric.Metrics) bool
776859
traces []ptrace.Traces
860+
statusCodeFeatureGate bool
777861
}{
778862
// disabling histogram
779863
{
@@ -828,6 +912,16 @@ func TestConsumeTraces(t *testing.T) {
828912
verifier: verifyBadMetricsOkay,
829913
traces: []ptrace.Traces{buildBadSampleTrace()},
830914
},
915+
{
916+
// Test that the status code is set properly
917+
name: "Test using new status code attribute",
918+
aggregationTemporality: delta,
919+
histogramConfig: exponentialHistogramsConfig,
920+
exemplarConfig: disabledExemplarsConfig,
921+
verifier: verifyOtelStatusCode,
922+
traces: []ptrace.Traces{appendTraceWithUnsetStatusCode(buildSampleTrace())},
923+
statusCodeFeatureGate: true,
924+
},
831925

832926
// explicit buckets histogram
833927
{
@@ -894,6 +988,12 @@ func TestConsumeTraces(t *testing.T) {
894988
require.NoError(t, err)
895989
// Override the default no-op consumer with metrics sink for testing.
896990
p.metricsConsumer = mcon
991+
if tc.statusCodeFeatureGate {
992+
require.NoError(t, featuregate.GlobalRegistry().Set(useOtelStatusCodeAttribute.ID(), true))
993+
defer func() {
994+
require.NoError(t, featuregate.GlobalRegistry().Set(useOtelStatusCodeAttribute.ID(), false))
995+
}()
996+
}
897997

898998
ctx := metadata.NewIncomingContext(t.Context(), nil)
899999
err = p.Start(ctx, componenttest.NewNopHost())

0 commit comments

Comments
 (0)