You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	feat: Improve error handling in prometheus exporter (#7363)
fix #7066 --------- Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
This commit is contained in:
		| @@ -42,6 +42,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm | |||||||
| - `WithInstrumentationAttributes` in `go.opentelemetry.io/otel/meter` synchronously de-duplicates the passed attributes instead of delegating it to the returned `MeterOption`. (#7266) | - `WithInstrumentationAttributes` in `go.opentelemetry.io/otel/meter` synchronously de-duplicates the passed attributes instead of delegating it to the returned `MeterOption`. (#7266) | ||||||
| - `WithInstrumentationAttributes` in `go.opentelemetry.io/otel/log` synchronously de-duplicates the passed attributes instead of delegating it to the returned `LoggerOption`. (#7266) | - `WithInstrumentationAttributes` in `go.opentelemetry.io/otel/log` synchronously de-duplicates the passed attributes instead of delegating it to the returned `LoggerOption`. (#7266) | ||||||
| - `Distinct` in `go.opentelemetry.io/otel/attribute` is no longer guaranteed to uniquely identify an attribute set. Collisions between `Distinct` values for different Sets are possible with extremely high cardinality (billions of series per instrument), but are highly unlikely. (#7175) | - `Distinct` in `go.opentelemetry.io/otel/attribute` is no longer guaranteed to uniquely identify an attribute set. Collisions between `Distinct` values for different Sets are possible with extremely high cardinality (billions of series per instrument), but are highly unlikely. (#7175) | ||||||
|  | - Improve error handling for dropped data during translation by using `prometheus.NewInvalidMetric` in `go.opentelemetry.io/otel/exporters/prometheus`. | ||||||
|  |   **Breaking Change:** Previously, these cases were only logged and scrapes succeeded. | ||||||
|  |   Now, when translation would drop data (e.g., invalid label/value), the exporter emits a `NewInvalidMetric`, and Prometheus scrapes **fail with HTTP 500** by default. | ||||||
|  |   To preserve the prior behavior (scrapes succeed while errors are logged), configure your Prometheus HTTP handler with: `promhttp.HandlerOpts{ ErrorHandling: promhttp.ContinueOnError }`. (#7363) | ||||||
| - The default `TranslationStrategy` in `go.opentelemetry.io/exporters/prometheus` is changed from `otlptranslator.NoUTF8EscapingWithSuffixes` to `otlptranslator.UnderscoreEscapingWithSuffixes`. (#7421) | - The default `TranslationStrategy` in `go.opentelemetry.io/exporters/prometheus` is changed from `otlptranslator.NoUTF8EscapingWithSuffixes` to `otlptranslator.UnderscoreEscapingWithSuffixes`. (#7421) | ||||||
| - The `ErrorType` function in `go.opentelemetry.io/otel/semconv/v1.37.0` now handles custom error types. | - The `ErrorType` function in `go.opentelemetry.io/otel/semconv/v1.37.0` now handles custom error types. | ||||||
|   If an error implements an `ErrorType() string` method, the return value of that method will be used as the error type. (#7442) |   If an error implements an `ErrorType() string` method, the return value of that method will be used as the error type. (#7442) | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								exporters/prometheus/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								exporters/prometheus/errors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | // Copyright The OpenTelemetry Authors | ||||||
|  | // SPDX-License-Identifier: Apache-2.0 | ||||||
|  |  | ||||||
|  | package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus" | ||||||
|  |  | ||||||
|  | import "errors" | ||||||
|  |  | ||||||
|  | // Sentinel errors for consistent error checks in tests. | ||||||
|  | var ( | ||||||
|  | 	errInvalidMetricType = errors.New("invalid metric type") | ||||||
|  | 	errInvalidMetric     = errors.New("invalid metric") | ||||||
|  | 	errEHScaleBelowMin   = errors.New("exponential histogram scale below minimum supported") | ||||||
|  | ) | ||||||
| @@ -241,7 +241,7 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) { | |||||||
|  |  | ||||||
| 			attrKeys, attrVals, e := getAttrs(scopeMetrics.Scope.Attributes, c.labelNamer) | 			attrKeys, attrVals, e := getAttrs(scopeMetrics.Scope.Attributes, c.labelNamer) | ||||||
| 			if e != nil { | 			if e != nil { | ||||||
| 				otel.Handle(e) | 				reportError(ch, nil, e) | ||||||
| 				err = errors.Join(err, fmt.Errorf("failed to getAttrs for ScopeMetrics %d: %w", j, e)) | 				err = errors.Join(err, fmt.Errorf("failed to getAttrs for ScopeMetrics %d: %w", j, e)) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| @@ -258,19 +258,19 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) { | |||||||
| 		for k, m := range scopeMetrics.Metrics { | 		for k, m := range scopeMetrics.Metrics { | ||||||
| 			typ := c.metricType(m) | 			typ := c.metricType(m) | ||||||
| 			if typ == nil { | 			if typ == nil { | ||||||
|  | 				reportError(ch, nil, errInvalidMetricType) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			name, e := c.getName(m) | 			name, e := c.getName(m) | ||||||
| 			if e != nil { | 			if e != nil { | ||||||
| 				// TODO(#7066): Handle this error better. It's not clear this can be | 				reportError(ch, nil, e) | ||||||
| 				// reached, bad metric names should / will be caught at creation time. |  | ||||||
| 				otel.Handle(e) |  | ||||||
| 				err = errors.Join(err, fmt.Errorf("failed to getAttrs for ScopeMetrics %d, Metrics %d: %w", j, k, e)) | 				err = errors.Join(err, fmt.Errorf("failed to getAttrs for ScopeMetrics %d, Metrics %d: %w", j, k, e)) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			drop, help := c.validateMetrics(name, m.Description, typ) | 			drop, help := c.validateMetrics(name, m.Description, typ) | ||||||
| 			if drop { | 			if drop { | ||||||
|  | 				reportError(ch, nil, errInvalidMetric) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -373,7 +373,7 @@ func addExponentialHistogramMetric[N int64 | float64]( | |||||||
| 	for j, dp := range histogram.DataPoints { | 	for j, dp := range histogram.DataPoints { | ||||||
| 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, nil, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for histogram.DataPoints %d: %w", j, e)) | 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for histogram.DataPoints %d: %w", j, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -386,11 +386,11 @@ func addExponentialHistogramMetric[N int64 | float64]( | |||||||
| 		scale := dp.Scale | 		scale := dp.Scale | ||||||
| 		if scale < -4 { | 		if scale < -4 { | ||||||
| 			// Reject scales below -4 as they cannot be represented in Prometheus | 			// Reject scales below -4 as they cannot be represented in Prometheus | ||||||
| 			e := fmt.Errorf( | 			reportError( | ||||||
| 				"exponential histogram scale %d is below minimum supported scale -4, skipping data point", | 				ch, | ||||||
| 				scale, | 				desc, | ||||||
|  | 				fmt.Errorf("%w: %d (min -4)", errEHScaleBelowMin, scale), | ||||||
| 			) | 			) | ||||||
| 			otel.Handle(e) |  | ||||||
| 			err = errors.Join(err, e) | 			err = errors.Join(err, e) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -440,7 +440,7 @@ func addExponentialHistogramMetric[N int64 | float64]( | |||||||
| 			dp.StartTime, | 			dp.StartTime, | ||||||
| 			values...) | 			values...) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, desc, e) | ||||||
| 			err = errors.Join( | 			err = errors.Join( | ||||||
| 				err, | 				err, | ||||||
| 				fmt.Errorf("failed to NewConstNativeHistogram for histogram.DataPoints %d: %w", j, e), | 				fmt.Errorf("failed to NewConstNativeHistogram for histogram.DataPoints %d: %w", j, e), | ||||||
| @@ -474,7 +474,7 @@ func addHistogramMetric[N int64 | float64]( | |||||||
| 	for j, dp := range histogram.DataPoints { | 	for j, dp := range histogram.DataPoints { | ||||||
| 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, nil, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for histogram.DataPoints %d: %w", j, e)) | 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for histogram.DataPoints %d: %w", j, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -491,7 +491,7 @@ func addHistogramMetric[N int64 | float64]( | |||||||
| 		} | 		} | ||||||
| 		m, e := prometheus.NewConstHistogram(desc, dp.Count, float64(dp.Sum), buckets, values...) | 		m, e := prometheus.NewConstHistogram(desc, dp.Count, float64(dp.Sum), buckets, values...) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, desc, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to NewConstMetric for histogram.DataPoints %d: %w", j, e)) | 			err = errors.Join(err, fmt.Errorf("failed to NewConstMetric for histogram.DataPoints %d: %w", j, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -527,7 +527,7 @@ func addSumMetric[N int64 | float64]( | |||||||
| 	for i, dp := range sum.DataPoints { | 	for i, dp := range sum.DataPoints { | ||||||
| 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, nil, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for sum.DataPoints %d: %w", i, e)) | 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for sum.DataPoints %d: %w", i, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -537,7 +537,7 @@ func addSumMetric[N int64 | float64]( | |||||||
| 		desc := prometheus.NewDesc(name, m.Description, keys, nil) | 		desc := prometheus.NewDesc(name, m.Description, keys, nil) | ||||||
| 		m, e := prometheus.NewConstMetric(desc, valueType, float64(dp.Value), values...) | 		m, e := prometheus.NewConstMetric(desc, valueType, float64(dp.Value), values...) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, desc, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to NewConstMetric for sum.DataPoints %d: %w", i, e)) | 			err = errors.Join(err, fmt.Errorf("failed to NewConstMetric for sum.DataPoints %d: %w", i, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -572,7 +572,7 @@ func addGaugeMetric[N int64 | float64]( | |||||||
| 	for i, dp := range gauge.DataPoints { | 	for i, dp := range gauge.DataPoints { | ||||||
| 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | 		keys, values, e := getAttrs(dp.Attributes, labelNamer) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, nil, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for gauge.DataPoints %d: %w", i, e)) | 			err = errors.Join(err, fmt.Errorf("failed to getAttrs for gauge.DataPoints %d: %w", i, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -582,7 +582,7 @@ func addGaugeMetric[N int64 | float64]( | |||||||
| 		desc := prometheus.NewDesc(name, m.Description, keys, nil) | 		desc := prometheus.NewDesc(name, m.Description, keys, nil) | ||||||
| 		m, e := prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(dp.Value), values...) | 		m, e := prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(dp.Value), values...) | ||||||
| 		if e != nil { | 		if e != nil { | ||||||
| 			otel.Handle(e) | 			reportError(ch, desc, e) | ||||||
| 			err = errors.Join(err, fmt.Errorf("failed to NewConstMetric for gauge.DataPoints %d: %w", i, e)) | 			err = errors.Join(err, fmt.Errorf("failed to NewConstMetric for gauge.DataPoints %d: %w", i, e)) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -803,3 +803,10 @@ func attributesToLabels(attrs []attribute.KeyValue, labelNamer otlptranslator.La | |||||||
| 	} | 	} | ||||||
| 	return labels, nil | 	return labels, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func reportError(ch chan<- prometheus.Metric, desc *prometheus.Desc, err error) { | ||||||
|  | 	if desc == nil { | ||||||
|  | 		desc = prometheus.NewInvalidDesc(err) | ||||||
|  | 	} | ||||||
|  | 	ch <- prometheus.NewInvalidMetric(desc, err) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -8,12 +8,15 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"math" | 	"math" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
| 	"os" | 	"os" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus" | 	"github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus/promhttp" | ||||||
| 	"github.com/prometheus/client_golang/prometheus/testutil" | 	"github.com/prometheus/client_golang/prometheus/testutil" | ||||||
| 	dto "github.com/prometheus/client_model/go" | 	dto "github.com/prometheus/client_model/go" | ||||||
| 	"github.com/prometheus/otlptranslator" | 	"github.com/prometheus/otlptranslator" | ||||||
| @@ -32,6 +35,27 @@ import ( | |||||||
| 	"go.opentelemetry.io/otel/trace" | 	"go.opentelemetry.io/otel/trace" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // producerFunc adapts a function to implement metric.Producer. | ||||||
|  | type producerFunc func(context.Context) ([]metricdata.ScopeMetrics, error) | ||||||
|  |  | ||||||
|  | func (f producerFunc) Produce(ctx context.Context) ([]metricdata.ScopeMetrics, error) { return f(ctx) } | ||||||
|  |  | ||||||
|  | // Helper: scrape with ContinueOnError and return body + status. | ||||||
|  | func scrapeWithContinueOnError(reg *prometheus.Registry) (int, string) { | ||||||
|  | 	h := promhttp.HandlerFor( | ||||||
|  | 		reg, | ||||||
|  | 		promhttp.HandlerOpts{ | ||||||
|  | 			ErrorHandling: promhttp.ContinueOnError, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	req := httptest.NewRequest(http.MethodGet, "/metrics", http.NoBody) | ||||||
|  | 	h.ServeHTTP(rr, req) | ||||||
|  |  | ||||||
|  | 	return rr.Code, rr.Body.String() | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestPrometheusExporter(t *testing.T) { | func TestPrometheusExporter(t *testing.T) { | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		name                string | 		name                string | ||||||
| @@ -786,6 +810,7 @@ func TestDuplicateMetrics(t *testing.T) { | |||||||
| 		recordMetrics         func(ctx context.Context, meterA, meterB otelmetric.Meter) | 		recordMetrics         func(ctx context.Context, meterA, meterB otelmetric.Meter) | ||||||
| 		options               []Option | 		options               []Option | ||||||
| 		possibleExpectedFiles []string | 		possibleExpectedFiles []string | ||||||
|  | 		expectGatherError     bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "no_conflict_two_counters", | 			name: "no_conflict_two_counters", | ||||||
| @@ -972,6 +997,7 @@ func TestDuplicateMetrics(t *testing.T) { | |||||||
| 				"testdata/conflict_type_counter_and_updowncounter_1.txt", | 				"testdata/conflict_type_counter_and_updowncounter_1.txt", | ||||||
| 				"testdata/conflict_type_counter_and_updowncounter_2.txt", | 				"testdata/conflict_type_counter_and_updowncounter_2.txt", | ||||||
| 			}, | 			}, | ||||||
|  | 			expectGatherError: true, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "conflict_type_histogram_and_updowncounter", | 			name: "conflict_type_histogram_and_updowncounter", | ||||||
| @@ -992,6 +1018,7 @@ func TestDuplicateMetrics(t *testing.T) { | |||||||
| 				"testdata/conflict_type_histogram_and_updowncounter_1.txt", | 				"testdata/conflict_type_histogram_and_updowncounter_1.txt", | ||||||
| 				"testdata/conflict_type_histogram_and_updowncounter_2.txt", | 				"testdata/conflict_type_histogram_and_updowncounter_2.txt", | ||||||
| 			}, | 			}, | ||||||
|  | 			expectGatherError: true, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -1032,19 +1059,43 @@ func TestDuplicateMetrics(t *testing.T) { | |||||||
|  |  | ||||||
| 			tc.recordMetrics(ctx, meterA, meterB) | 			tc.recordMetrics(ctx, meterA, meterB) | ||||||
|  |  | ||||||
| 			match := false | 			if tc.expectGatherError { | ||||||
| 			for _, filename := range tc.possibleExpectedFiles { | 				// With improved error handling, conflicting instrument types emit an invalid metric. | ||||||
| 				file, ferr := os.Open(filename) | 				// Gathering should surface an error instead of silently dropping. | ||||||
| 				require.NoError(t, ferr) | 				_, err := registry.Gather() | ||||||
| 				t.Cleanup(func() { require.NoError(t, file.Close()) }) | 				require.Error(t, err) | ||||||
|  |  | ||||||
| 				err = testutil.GatherAndCompare(registry, file) | 				// 2) Also assert what users will see if they opt into ContinueOnError. | ||||||
| 				if err == nil { | 				// Compare the HTTP body to an expected file that contains only the valid series | ||||||
| 					match = true | 				// (e.g., "target_info" and any non-conflicting families). | ||||||
| 					break | 				status, body := scrapeWithContinueOnError(registry) | ||||||
|  | 				require.Equal(t, http.StatusOK, status) | ||||||
|  |  | ||||||
|  | 				matched := false | ||||||
|  | 				for _, filename := range tc.possibleExpectedFiles { | ||||||
|  | 					want, ferr := os.ReadFile(filename) | ||||||
|  | 					require.NoError(t, ferr) | ||||||
|  | 					if body == string(want) { | ||||||
|  | 						matched = true | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
|  | 				require.Truef(t, matched, "expected export not produced under ContinueOnError; got:\n%s", body) | ||||||
|  | 			} else { | ||||||
|  | 				match := false | ||||||
|  | 				for _, filename := range tc.possibleExpectedFiles { | ||||||
|  | 					file, ferr := os.Open(filename) | ||||||
|  | 					require.NoError(t, ferr) | ||||||
|  | 					t.Cleanup(func() { require.NoError(t, file.Close()) }) | ||||||
|  |  | ||||||
|  | 					err = testutil.GatherAndCompare(registry, file) | ||||||
|  | 					if err == nil { | ||||||
|  | 						match = true | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				require.Truef(t, match, "expected export not produced: %v", err) | ||||||
| 			} | 			} | ||||||
| 			require.Truef(t, match, "expected export not produced: %v", err) |  | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -1378,14 +1429,18 @@ func TestExponentialHistogramScaleValidation(t *testing.T) { | |||||||
| 			nil, | 			nil, | ||||||
| 			t.Context(), | 			t.Context(), | ||||||
| 		) | 		) | ||||||
| 		assert.Error(t, capturedError) | 		// Expect an invalid metric to be sent that carries the scale error. | ||||||
| 		assert.Contains(t, capturedError.Error(), "scale -5 is below minimum") | 		var pm prometheus.Metric | ||||||
| 		select { | 		select { | ||||||
| 		case <-ch: | 		case pm = <-ch: | ||||||
| 			t.Error("Expected no metrics to be produced for invalid scale") |  | ||||||
| 		default: | 		default: | ||||||
| 			// No metrics were produced for the invalid scale | 			t.Fatalf("expected an invalid metric to be emitted for invalid scale, but channel was empty") | ||||||
| 		} | 		} | ||||||
|  | 		var dtoMetric dto.Metric | ||||||
|  | 		werr := pm.Write(&dtoMetric) | ||||||
|  | 		require.ErrorIs(t, werr, errEHScaleBelowMin) | ||||||
|  | 		// The exporter reports via invalid metric, not the global otel error handler. | ||||||
|  | 		assert.NoError(t, capturedError) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1784,16 +1839,114 @@ func TestDownscaleExponentialBucketEdgeCases(t *testing.T) { | |||||||
| // TestEscapingErrorHandling increases test coverage by exercising some error | // TestEscapingErrorHandling increases test coverage by exercising some error | ||||||
| // conditions. | // conditions. | ||||||
| func TestEscapingErrorHandling(t *testing.T) { | func TestEscapingErrorHandling(t *testing.T) { | ||||||
|  | 	// Helper to create a producer that emits a Summary (unsupported) metric. | ||||||
|  | 	makeSummaryProducer := func() metric.Producer { | ||||||
|  | 		return producerFunc(func(_ context.Context) ([]metricdata.ScopeMetrics, error) { | ||||||
|  | 			return []metricdata.ScopeMetrics{ | ||||||
|  | 				{ | ||||||
|  | 					Metrics: []metricdata.Metrics{ | ||||||
|  | 						{ | ||||||
|  | 							Name:        "summary_metric", | ||||||
|  | 							Description: "unsupported summary", | ||||||
|  | 							Data:        metricdata.Summary{}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, nil | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	// Helper to create a producer that emits a metric with an invalid name, to | ||||||
|  | 	// force getName() to fail and exercise reportError at that branch. | ||||||
|  | 	makeBadNameProducer := func() metric.Producer { | ||||||
|  | 		return producerFunc(func(_ context.Context) ([]metricdata.ScopeMetrics, error) { | ||||||
|  | 			return []metricdata.ScopeMetrics{ | ||||||
|  | 				{ | ||||||
|  | 					Metrics: []metricdata.Metrics{ | ||||||
|  | 						{ | ||||||
|  | 							Name:        "$%^&", // intentionally invalid; translation should fail normalization | ||||||
|  | 							Description: "bad name for translation", | ||||||
|  | 							// Any supported type is fine; getName runs before add* functions. | ||||||
|  | 							Data: metricdata.Gauge[float64]{ | ||||||
|  | 								DataPoints: []metricdata.DataPoint[float64]{ | ||||||
|  | 									{Value: 1}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, nil | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	// Helper to create a producer that emits an ExponentialHistogram with a bad | ||||||
|  | 	// label, to exercise addExponentialHistogramMetric getAttrs error path. | ||||||
|  | 	makeBadEHProducer := func() metric.Producer { | ||||||
|  | 		return producerFunc(func(_ context.Context) ([]metricdata.ScopeMetrics, error) { | ||||||
|  | 			return []metricdata.ScopeMetrics{ | ||||||
|  | 				{ | ||||||
|  | 					Metrics: []metricdata.Metrics{ | ||||||
|  | 						{ | ||||||
|  | 							Name:        "exp_hist_metric", | ||||||
|  | 							Description: "bad label", | ||||||
|  | 							Data: metricdata.ExponentialHistogram[float64]{ | ||||||
|  | 								DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{ | ||||||
|  | 									{ | ||||||
|  | 										Attributes:    attribute.NewSet(attribute.Key("$%^&").String("B")), | ||||||
|  | 										Scale:         0, | ||||||
|  | 										Count:         1, | ||||||
|  | 										ZeroThreshold: 0, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, nil | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	// Helper to create a producer that emits an ExponentialHistogram with | ||||||
|  | 	// inconsistent bucket counts vs total Count to trigger constructor error in addExponentialHistogramMetric. | ||||||
|  | 	makeBadEHCountProducer := func() metric.Producer { | ||||||
|  | 		return producerFunc(func(_ context.Context) ([]metricdata.ScopeMetrics, error) { | ||||||
|  | 			return []metricdata.ScopeMetrics{ | ||||||
|  | 				{ | ||||||
|  | 					Metrics: []metricdata.Metrics{ | ||||||
|  | 						{ | ||||||
|  | 							Name: "exp_hist_metric_bad", | ||||||
|  | 							Data: metricdata.ExponentialHistogram[float64]{ | ||||||
|  | 								DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{ | ||||||
|  | 									{ | ||||||
|  | 										Scale:         0, | ||||||
|  | 										Count:         0, | ||||||
|  | 										ZeroThreshold: 0, | ||||||
|  | 										PositiveBucket: metricdata.ExponentialBucket{ | ||||||
|  | 											Offset: 0, | ||||||
|  | 											Counts: []uint64{1}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, nil | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		name                string | 		name                    string | ||||||
| 		namespace           string | 		namespace               string | ||||||
| 		counterName         string | 		counterName             string | ||||||
| 		customScopeAttrs    []attribute.KeyValue | 		customScopeAttrs        []attribute.KeyValue | ||||||
| 		customResourceAttrs []attribute.KeyValue | 		customResourceAttrs     []attribute.KeyValue | ||||||
| 		labelName           string | 		labelName               string | ||||||
| 		expectNewErr        string | 		producer                metric.Producer | ||||||
| 		expectMetricErr     string | 		skipInstrument          bool | ||||||
| 		checkMetricFamilies func(t testing.TB, dtos []*dto.MetricFamily) | 		record                  func(ctx context.Context, meter otelmetric.Meter) error | ||||||
|  | 		expectNewErr            string | ||||||
|  | 		expectMetricErr         string | ||||||
|  | 		expectGatherErrContains string | ||||||
|  | 		expectGatherErrIs       error | ||||||
|  | 		checkMetricFamilies     func(t testing.TB, dtos []*dto.MetricFamily) | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name:        "simple happy path", | 			name:        "simple happy path", | ||||||
| @@ -1814,6 +1967,29 @@ func TestEscapingErrorHandling(t *testing.T) { | |||||||
| 			counterName:  "foo", | 			counterName:  "foo", | ||||||
| 			expectNewErr: `normalization for label name "$%^&" resulted in invalid name "_"`, | 			expectNewErr: `normalization for label name "$%^&" resulted in invalid name "_"`, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "bad translated metric name via producer", | ||||||
|  | 			// Use a producer to emit a metric with an invalid name to trigger getName error. | ||||||
|  | 			producer:       makeBadNameProducer(), | ||||||
|  | 			skipInstrument: true, | ||||||
|  | 			// Error message comes from normalization in the translator; match on a stable substring. | ||||||
|  | 			expectGatherErrContains: "normalization", | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				// target_info should still be exported. | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:        "good namespace, names should be escaped", | 			name:        "good namespace, names should be escaped", | ||||||
| 			namespace:   "my-strange-namespace", | 			namespace:   "my-strange-namespace", | ||||||
| @@ -1845,9 +2021,23 @@ func TestEscapingErrorHandling(t *testing.T) { | |||||||
| 			customScopeAttrs: []attribute.KeyValue{ | 			customScopeAttrs: []attribute.KeyValue{ | ||||||
| 				attribute.Key("$%^&").String("B"), | 				attribute.Key("$%^&").String("B"), | ||||||
| 			}, | 			}, | ||||||
|  | 			// With improved error handling, invalid scope label names result in an invalid metric | ||||||
|  | 			// and Gather returns an error containing the normalization failure. | ||||||
|  | 			expectGatherErrContains: `normalization for label name "$%^&" resulted in invalid name "_"`, | ||||||
| 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
| 				require.Len(t, mfs, 1) | 				// target_info should still be exported; metric with bad scope label dropped. | ||||||
| 				require.Equal(t, "target_info", mfs[0].GetName()) | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| @@ -1857,14 +2047,203 @@ func TestEscapingErrorHandling(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			// label names are not translated and therefore not checked until | 			// label names are not translated and therefore not checked until | ||||||
| 			// collection time, and there is no place to catch and return this error. | 			// collection time; with improved error handling, we emit an invalid metric and | ||||||
| 			// Instead we drop the metric. | 			// surface the error during Gather. | ||||||
| 			name:        "bad translated label name", | 			name:                    "bad translated label name", | ||||||
| 			counterName: "foo", | 			counterName:             "foo", | ||||||
| 			labelName:   "$%^&", | 			labelName:               "$%^&", | ||||||
|  | 			expectGatherErrContains: `normalization for label name "$%^&" resulted in invalid name "_"`, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "unsupported data type via producer", | ||||||
|  | 			// Use a producer to emit a Summary data point; no SDK instruments. | ||||||
|  | 			producer:          makeSummaryProducer(), | ||||||
|  | 			skipInstrument:    true, | ||||||
|  | 			expectGatherErrIs: errInvalidMetricType, | ||||||
| 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
| 				require.Len(t, mfs, 1) | 				require.NotEmpty(t, mfs) | ||||||
| 				require.Equal(t, "target_info", mfs[0].GetName()) | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                    "bad exponential histogram label name via producer", | ||||||
|  | 			producer:                makeBadEHProducer(), | ||||||
|  | 			skipInstrument:          true, | ||||||
|  | 			expectGatherErrContains: `normalization for label name "$%^&" resulted in invalid name "_"`, | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                    "exponential histogram constructor error via producer (count mismatch)", | ||||||
|  | 			producer:                makeBadEHCountProducer(), | ||||||
|  | 			skipInstrument:          true, | ||||||
|  | 			expectGatherErrContains: "count", | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "sum constructor error via duplicate label name", | ||||||
|  | 			record: func(ctx context.Context, meter otelmetric.Meter) error { | ||||||
|  | 				c, err := meter.Int64Counter("sum_metric_dup") | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				// Duplicate variable label name with scope label to make Desc invalid. | ||||||
|  | 				c.Add(ctx, 1, otelmetric.WithAttributes(attribute.String(scopeNameLabel, "x"))) | ||||||
|  | 				return nil | ||||||
|  | 			}, | ||||||
|  | 			expectGatherErrContains: "duplicate label", | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "gauge constructor error via duplicate label name", | ||||||
|  | 			record: func(ctx context.Context, meter otelmetric.Meter) error { | ||||||
|  | 				g, err := meter.Float64Gauge("gauge_metric_dup") | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				g.Record(ctx, 1.0, otelmetric.WithAttributes(attribute.String(scopeNameLabel, "x"))) | ||||||
|  | 				return nil | ||||||
|  | 			}, | ||||||
|  | 			expectGatherErrContains: "duplicate label", | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "histogram constructor error via duplicate label name", | ||||||
|  | 			record: func(ctx context.Context, meter otelmetric.Meter) error { | ||||||
|  | 				h, err := meter.Float64Histogram("hist_metric_dup") | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				h.Record(ctx, 1.23, otelmetric.WithAttributes(attribute.String(scopeNameLabel, "x"))) | ||||||
|  | 				return nil | ||||||
|  | 			}, | ||||||
|  | 			expectGatherErrContains: "duplicate label", | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "bad gauge label name", | ||||||
|  | 			record: func(ctx context.Context, meter otelmetric.Meter) error { | ||||||
|  | 				g, err := meter.Float64Gauge("gauge_metric") | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				g.Record(ctx, 1, otelmetric.WithAttributes(attribute.Key("$%^&").String("B"))) | ||||||
|  | 				return nil | ||||||
|  | 			}, | ||||||
|  | 			expectGatherErrContains: `normalization for label name "$%^&" resulted in invalid name "_"`, | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "bad histogram label name", | ||||||
|  | 			record: func(ctx context.Context, meter otelmetric.Meter) error { | ||||||
|  | 				h, err := meter.Float64Histogram("hist_metric") | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				h.Record(ctx, 1.23, otelmetric.WithAttributes(attribute.Key("$%^&").String("B"))) | ||||||
|  | 				return nil | ||||||
|  | 			}, | ||||||
|  | 			expectGatherErrContains: `normalization for label name "$%^&" resulted in invalid name "_"`, | ||||||
|  | 			checkMetricFamilies: func(t testing.TB, mfs []*dto.MetricFamily) { | ||||||
|  | 				require.NotEmpty(t, mfs) | ||||||
|  | 				other := 0 | ||||||
|  | 				seenTarget := false | ||||||
|  | 				for _, mf := range mfs { | ||||||
|  | 					if mf.GetName() == "target_info" { | ||||||
|  | 						seenTarget = true | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					other++ | ||||||
|  | 				} | ||||||
|  | 				require.True(t, seenTarget) | ||||||
|  | 				require.Equal(t, 0, other) | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| @@ -1881,49 +2260,70 @@ func TestEscapingErrorHandling(t *testing.T) { | |||||||
| 			}) | 			}) | ||||||
| 			ctx = trace.ContextWithSpanContext(ctx, sc) | 			ctx = trace.ContextWithSpanContext(ctx, sc) | ||||||
|  |  | ||||||
| 			exporter, err := New( | 			opts := []Option{ | ||||||
| 				WithRegisterer(registry), | 				WithRegisterer(registry), | ||||||
| 				WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes), | 				WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes), | ||||||
| 				WithNamespace(tc.namespace), | 				WithNamespace(tc.namespace), | ||||||
| 				WithResourceAsConstantLabels(attribute.NewDenyKeysFilter()), | 				WithResourceAsConstantLabels(attribute.NewDenyKeysFilter()), | ||||||
| 			) | 			} | ||||||
|  | 			if tc.producer != nil { | ||||||
|  | 				opts = append(opts, WithProducer(tc.producer)) | ||||||
|  | 			} | ||||||
|  | 			exporter, err := New(opts...) | ||||||
| 			if tc.expectNewErr != "" { | 			if tc.expectNewErr != "" { | ||||||
| 				require.ErrorContains(t, err, tc.expectNewErr) | 				require.ErrorContains(t, err, tc.expectNewErr) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
|  | 			if !tc.skipInstrument { | ||||||
| 			res, err := resource.New(ctx, | 				res, err := resource.New(ctx, | ||||||
| 				resource.WithAttributes(semconv.ServiceName("prometheus_test")), | 					resource.WithAttributes(semconv.ServiceName("prometheus_test")), | ||||||
| 				resource.WithAttributes(semconv.TelemetrySDKVersion("latest")), | 					resource.WithAttributes(semconv.TelemetrySDKVersion("latest")), | ||||||
| 				resource.WithAttributes(tc.customResourceAttrs...), | 					resource.WithAttributes(tc.customResourceAttrs...), | ||||||
| 			) | 				) | ||||||
| 			require.NoError(t, err) | 				require.NoError(t, err) | ||||||
| 			provider := metric.NewMeterProvider( | 				provider := metric.NewMeterProvider( | ||||||
| 				metric.WithReader(exporter), | 					metric.WithReader(exporter), | ||||||
| 				metric.WithResource(res), | 					metric.WithResource(res), | ||||||
| 			) | 				) | ||||||
|  | 				meter := provider.Meter( | ||||||
| 			fooCounter, err := provider.Meter( | 					"meterfoo", | ||||||
| 				"meterfoo", | 					otelmetric.WithInstrumentationVersion("v0.1.0"), | ||||||
| 				otelmetric.WithInstrumentationVersion("v0.1.0"), | 					otelmetric.WithInstrumentationAttributes(tc.customScopeAttrs...), | ||||||
| 				otelmetric.WithInstrumentationAttributes(tc.customScopeAttrs...), | 				) | ||||||
| 			). | 				if tc.record != nil { | ||||||
| 				Int64Counter( | 					err := tc.record(ctx, meter) | ||||||
| 					tc.counterName, | 					require.NoError(t, err) | ||||||
| 					otelmetric.WithUnit("s"), | 				} else { | ||||||
| 					otelmetric.WithDescription(fmt.Sprintf(`meter %q counter`, tc.counterName))) | 					fooCounter, err := meter.Int64Counter( | ||||||
| 			if tc.expectMetricErr != "" { | 						tc.counterName, | ||||||
| 				require.ErrorContains(t, err, tc.expectMetricErr) | 						otelmetric.WithUnit("s"), | ||||||
|  | 						otelmetric.WithDescription(fmt.Sprintf(`meter %q counter`, tc.counterName))) | ||||||
|  | 					if tc.expectMetricErr != "" { | ||||||
|  | 						require.ErrorContains(t, err, tc.expectMetricErr) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 					require.NoError(t, err) | ||||||
|  | 					var addOpts []otelmetric.AddOption | ||||||
|  | 					if tc.labelName != "" { | ||||||
|  | 						addOpts = append(addOpts, otelmetric.WithAttributes(attribute.String(tc.labelName, "foo"))) | ||||||
|  | 					} | ||||||
|  | 					fooCounter.Add(ctx, 100, addOpts...) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				// When skipping instruments, still register the reader so Collect will run. | ||||||
|  | 				_ = metric.NewMeterProvider(metric.WithReader(exporter)) | ||||||
|  | 			} | ||||||
|  | 			got, err := registry.Gather() | ||||||
|  | 			if tc.expectGatherErrContains != "" { | ||||||
|  | 				require.Error(t, err) | ||||||
|  | 				require.Contains(t, err.Error(), tc.expectGatherErrContains) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			require.NoError(t, err) | 			if tc.expectGatherErrIs != nil { | ||||||
| 			var opts []otelmetric.AddOption | 				require.ErrorIs(t, err, tc.expectGatherErrIs) | ||||||
| 			if tc.labelName != "" { | 				return | ||||||
| 				opts = append(opts, otelmetric.WithAttributes(attribute.String(tc.labelName, "foo"))) |  | ||||||
| 			} | 			} | ||||||
| 			fooCounter.Add(ctx, 100, opts...) |  | ||||||
| 			got, err := registry.Gather() |  | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
| 			if tc.checkMetricFamilies != nil { | 			if tc.checkMetricFamilies != nil { | ||||||
| 				tc.checkMetricFamilies(t, got) | 				tc.checkMetricFamilies(t, got) | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | |||||||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= | ||||||
|  | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= | ||||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||||||
| github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | ||||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user