|
8 | 8 | "compress/gzip" |
9 | 9 | "context" |
10 | 10 | "crypto/tls" |
| 11 | + "errors" |
11 | 12 | "fmt" |
12 | 13 | "io" |
13 | 14 | "net/http" |
@@ -818,3 +819,79 @@ func BenchmarkExporterExportSpans(b *testing.B) { |
818 | 819 | run(b) |
819 | 820 | }) |
820 | 821 | } |
| 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 | +} |
0 commit comments