Skip to content

Commit 4b0f509

Browse files
authored
Merge branch 'main' into fix/processconv-observable-metrics
2 parents bed5db7 + 5bd5702 commit 4b0f509

7 files changed

Lines changed: 161 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
4848
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8152)
4949
- `go.opentelemetry.io/otel/exporters/prometheus` now uses `Value.String` formatting for label values following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8170)
5050
- Propagate errors from the exporter when calling `Shutdown` on `BatchSpanProcessor` in `go.opentelemetry.io/otel/sdk/trace`. (#8197)
51+
- Fix stale status code reporting on self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8226)
5152
- Fix a concurrent `Collect` data race and potential panic in `go.opentelemetry.io/otel/exporters/prometheus` when `WithResourceAsConstantLabels` option is used. (#8227)
5253

5354
<!-- Released section -->

exporters/otlp/otlplog/otlploghttp/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ func (c *httpClient) uploadLogs(ctx context.Context, data []*logpb.ResourceLogs)
177177
default:
178178
}
179179

180+
statusCode = 0
180181
request.reset(iCtx)
181182
// nolint:gosec // URL is constructed from validated OTLP endpoint configuration
182183
resp, err := c.client.Do(request.Request)

exporters/otlp/otlplog/otlploghttp/client_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,3 +1183,76 @@ func BenchmarkExporterExportLogs(b *testing.B) {
11831183

11841184
b.Run("NoObservability", run)
11851185
}
1186+
1187+
func TestClientInstrumentationStaleStatusCode(t *testing.T) {
1188+
t.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
1189+
const id = 0
1190+
SetExporterID(id)
1191+
1192+
orig := otel.GetMeterProvider()
1193+
t.Cleanup(func() { otel.SetMeterProvider(orig) })
1194+
1195+
reader := metric.NewManualReader()
1196+
mp := metric.NewMeterProvider(metric.WithReader(reader))
1197+
otel.SetMeterProvider(mp)
1198+
1199+
// Use a client that returns a 503 error once and then a network error.
1200+
var calls int
1201+
client := &http.Client{
1202+
Transport: roundTripperFunc(func(_ *http.Request) (*http.Response, error) {
1203+
calls++
1204+
if calls == 1 {
1205+
return &http.Response{
1206+
StatusCode: http.StatusServiceUnavailable,
1207+
Status: "503 " + http.StatusText(http.StatusServiceUnavailable),
1208+
Header: make(http.Header),
1209+
Body: io.NopCloser(strings.NewReader("")),
1210+
}, nil
1211+
}
1212+
return nil, errors.New("network error")
1213+
}),
1214+
}
1215+
1216+
opts := []Option{
1217+
WithHTTPClient(client),
1218+
WithInsecure(),
1219+
WithRetry(RetryConfig{
1220+
Enabled: true,
1221+
InitialInterval: time.Nanosecond,
1222+
MaxInterval: time.Nanosecond,
1223+
MaxElapsedTime: time.Second,
1224+
}),
1225+
}
1226+
cfg := newConfig(opts)
1227+
c, err := newHTTPClient(t.Context(), cfg)
1228+
require.NoError(t, err)
1229+
1230+
err = c.UploadLogs(t.Context(), resourceLogs)
1231+
assert.Error(t, err)
1232+
1233+
// Validate that the status code is 0 and not the stale 503 on self-observability metrics.
1234+
var got metricdata.ResourceMetrics
1235+
require.NoError(t, reader.Collect(t.Context(), &got))
1236+
1237+
require.Len(t, got.ScopeMetrics, 1)
1238+
metrics := got.ScopeMetrics[0].Metrics
1239+
var found bool
1240+
for _, m := range metrics {
1241+
if m.Name != (otelconv.SDKExporterOperationDuration{}).Name() {
1242+
continue
1243+
}
1244+
found = true
1245+
data := m.Data.(metricdata.Histogram[float64])
1246+
require.NotEmpty(t, data.DataPoints)
1247+
dp := data.DataPoints[0]
1248+
_, ok := dp.Attributes.Value(otelconv.SDKExporterOperationDuration{}.AttrHTTPResponseStatusCode(0).Key)
1249+
assert.False(t, ok, "should not report status code when the request fails before getting a response.")
1250+
}
1251+
assert.True(t, found, "expected to find operation duration metric")
1252+
}
1253+
1254+
type roundTripperFunc func(*http.Request) (*http.Response, error)
1255+
1256+
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
1257+
return f(r)
1258+
}

exporters/otlp/otlplog/otlploghttp/internal/observ/instrumentation.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,10 @@ func (e ExportOp) recordOption(err error, code int) metric.RecordOption {
268268
defer put(attrsPool, attrs)
269269

270270
*attrs = append(*attrs, e.inst.presetAttrs...)
271-
*attrs = append(
272-
*attrs,
273-
semconv.HTTPResponseStatusCode(code),
274-
semconv.ErrorType(err),
275-
)
271+
if code != 0 {
272+
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(code))
273+
}
274+
*attrs = append(*attrs, semconv.ErrorType(err))
276275
return metric.WithAttributeSet(attribute.NewSet(*attrs...))
277276
}
278277

exporters/otlp/otlptrace/otlptracehttp/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
186186
default:
187187
}
188188

189+
statusCode = 0
189190
request.reset(ctx)
190191
// nolint:gosec // URL is constructed from validated OTLP endpoint configuration
191192
resp, err := d.client.Do(request.Request)

exporters/otlp/otlptrace/otlptracehttp/client_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"compress/gzip"
99
"context"
1010
"crypto/tls"
11+
"errors"
1112
"fmt"
1213
"io"
1314
"net/http"
@@ -818,3 +819,79 @@ func BenchmarkExporterExportSpans(b *testing.B) {
818819
run(b)
819820
})
820821
}
822+
823+
func TestClientInstrumentationStaleStatusCode(t *testing.T) {
824+
t.Setenv("OTEL_GO_X_OBSERVABILITY", "true")
825+
const id = 0
826+
counter.SetExporterID(id)
827+
828+
orig := otel.GetMeterProvider()
829+
t.Cleanup(func() { otel.SetMeterProvider(orig) })
830+
831+
reader := metric.NewManualReader()
832+
mp := metric.NewMeterProvider(metric.WithReader(reader))
833+
otel.SetMeterProvider(mp)
834+
835+
// Use a client that returns a 503 error once and then a network error.
836+
var calls int
837+
client := &http.Client{
838+
Transport: roundTripperFunc(func(_ *http.Request) (*http.Response, error) {
839+
calls++
840+
if calls == 1 {
841+
return &http.Response{
842+
StatusCode: http.StatusServiceUnavailable,
843+
Status: fmt.Sprintf("%d %s",
844+
http.StatusServiceUnavailable,
845+
http.StatusText(http.StatusServiceUnavailable)),
846+
Header: make(http.Header),
847+
Body: io.NopCloser(strings.NewReader("")),
848+
}, nil
849+
}
850+
return nil, errors.New("network error")
851+
}),
852+
}
853+
854+
driver := otlptracehttp.NewClient(
855+
otlptracehttp.WithHTTPClient(client),
856+
otlptracehttp.WithInsecure(),
857+
otlptracehttp.WithRetry(otlptracehttp.RetryConfig{
858+
Enabled: true,
859+
InitialInterval: time.Nanosecond,
860+
MaxInterval: time.Nanosecond,
861+
MaxElapsedTime: time.Second,
862+
}),
863+
)
864+
exporter, err := otlptrace.New(t.Context(), driver)
865+
require.NoError(t, err)
866+
867+
err = exporter.ExportSpans(t.Context(), otlptracetest.SingleReadOnlySpan())
868+
assert.Error(t, err)
869+
870+
require.NoError(t, exporter.Shutdown(t.Context()))
871+
872+
// Validate that the status code is 0 and not the stale 503 on self-observability metrics.
873+
var got metricdata.ResourceMetrics
874+
require.NoError(t, reader.Collect(t.Context(), &got))
875+
876+
require.Len(t, got.ScopeMetrics, 1)
877+
metrics := got.ScopeMetrics[0].Metrics
878+
var found bool
879+
for _, m := range metrics {
880+
if m.Name != (otelconv.SDKExporterOperationDuration{}).Name() {
881+
continue
882+
}
883+
found = true
884+
data := m.Data.(metricdata.Histogram[float64])
885+
require.NotEmpty(t, data.DataPoints)
886+
dp := data.DataPoints[0]
887+
_, ok := dp.Attributes.Value(otelconv.SDKExporterOperationDuration{}.AttrHTTPResponseStatusCode(0).Key)
888+
assert.False(t, ok, "should not report status code when the request fails before getting a response.")
889+
}
890+
assert.True(t, found, "expected to find operation duration metric")
891+
}
892+
893+
type roundTripperFunc func(*http.Request) (*http.Response, error)
894+
895+
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
896+
return f(r)
897+
}

exporters/otlp/otlptrace/otlptracehttp/internal/observ/instrumentation.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ func (e ExportOp) End(err error, status int) {
345345
//
346346
// Otherwise, a new RecordOption is returned with the base attributes of the
347347
// Instrumentation plus the http.response.status_code attribute set to the
348-
// provided status, and if err is not nil, the error.type attribute set
348+
// provided status (if non-zero), and if err is not nil, the error.type attribute set
349349
// to the type of the error.
350350
func (i *Instrumentation) recordOption(err error, status int) metric.RecordOption {
351351
if err == nil && status == http.StatusOK {
@@ -356,7 +356,9 @@ func (i *Instrumentation) recordOption(err error, status int) metric.RecordOptio
356356
defer put(measureAttrsPool, attrs)
357357
*attrs = append(*attrs, i.attrs...)
358358

359-
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
359+
if status != 0 {
360+
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
361+
}
360362
if err != nil {
361363
*attrs = append(*attrs, semconv.ErrorType(err))
362364
}

0 commit comments

Comments
 (0)