diff --git a/.chloggen/prometheusexporter-translation-strategies.yaml b/.chloggen/prometheusexporter-translation-strategies.yaml new file mode 100644 index 0000000000000..5671cbf192bc2 --- /dev/null +++ b/.chloggen/prometheusexporter-translation-strategies.yaml @@ -0,0 +1,34 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: exporter/prometheus + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add `translation_strategy` configuration option to control how OTLP metric names are translated to Prometheus format. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [35459] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + The new `translation_strategy` option provides four different translation modes: + - `UnderscoreEscapingWithSuffixes`: Escapes special characters to underscores and appends type/unit suffixes + - `UnderscoreEscapingWithoutSuffixes`: Escapes special characters but omits suffixes + - `NoUTF8EscapingWithSuffixes`: Preserves UTF-8 characters while adding suffixes + - `NoTranslation`: Passes metric names through unaltered + When `translation_strategy` is set, it always takes precedence over the deprecated `add_metric_suffixes` option. + The `exporter.prometheusexporter.DisableAddMetricSuffixes` feature gate can be used to completely ignore the deprecated `add_metric_suffixes` setting. + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/exporter/prometheusexporter/README.md b/exporter/prometheusexporter/README.md index c2303832f4680..5e26aeabc225e 100644 --- a/exporter/prometheusexporter/README.md +++ b/exporter/prometheusexporter/README.md @@ -31,7 +31,12 @@ The following settings can be optionally configured: - `resource_to_telemetry_conversion` - `enabled` (default = false): If `enabled` is `true`, all the resource attributes will be converted to metric labels by default. - `enable_open_metrics`: (default = `false`): If true, metrics will be exported using the OpenMetrics format. Exemplars are only exported in the OpenMetrics format, and only for histogram and monotonic sum (i.e. counter) metrics. -- `add_metric_suffixes`: (default = `true`): If false, addition of type and unit suffixes is disabled. +- `add_metric_suffixes`: (default = `true`): If false, addition of type and unit suffixes is disabled. **Deprecated**: Use `translation_strategy` instead. This setting is ignored when `translation_strategy` is explicitly set. +- `translation_strategy`: Controls how OTLP metric and attribute names are translated into Prometheus metric and label names. When set, this takes precedence over `add_metric_suffixes`. Available options: + - `UnderscoreEscapingWithSuffixes`: Fully escapes metric names for classic Prometheus metric name compatibility, and includes appending type and unit suffixes. + - `UnderscoreEscapingWithoutSuffixes`: Metric names will continue to escape special characters to `_`, but suffixes won't be attached. + - `NoUTF8EscapingWithSuffixes`: Disables changing special characters to `_`. Special suffixes like units and `_total` for counters will be attached. + - `NoTranslation`: Bypasses all metric and label name translation, passing them through unaltered. Example: @@ -50,7 +55,9 @@ exporters: send_timestamps: true metric_expiration: 180m enable_open_metrics: true + # Legacy configuration - deprecated, ignored when translation_strategy is set add_metric_suffixes: false + translation_strategy: "UnderscoreEscapingWithoutSuffixes" resource_to_telemetry_conversion: enabled: true ``` @@ -59,7 +66,9 @@ Given the example, metrics will be available at `https://1.2.3.4:1234/metrics`. ## Metric names and labels normalization -OpenTelemetry metric names and attributes are normalized to be compliant with Prometheus naming rules. [Details on this normalization process are described in the Prometheus translator module](../../pkg/translator/prometheus/). +By Default, OpenTelemetry metric names and attributes are normalized to be compliant with [Prometheus naming rules](https://prometheus.io/docs/practices/naming/). + +Optionally, users can set different `translation_strategy` options to control how metrics are exposed. Please be aware that Prometheus itself uses content negotiation to decide how to ingest metrics, and underscore escaping might be applied even though this exporter is configured to keep UTF-8 characters. For more details, read [Prometheus' Content Negotiation documentation](https://prometheus.io/docs/instrumenting/content_negotiation/). ## Setting resource attributes as metric labels diff --git a/exporter/prometheusexporter/collector.go b/exporter/prometheusexporter/collector.go index 9bfe3dbe9f494..741454bbc8cbe 100644 --- a/exporter/prometheusexporter/collector.go +++ b/exporter/prometheusexporter/collector.go @@ -31,12 +31,11 @@ type collector struct { accumulator accumulator logger *zap.Logger - sendTimestamps bool - addMetricSuffixes bool - namespace string - constLabels prometheus.Labels - metricFamilies sync.Map - metricExpiration time.Duration + sendTimestamps bool + namespace string + constLabels prometheus.Labels + metricFamilies sync.Map + metricExpiration time.Duration metricNamer otlptranslator.MetricNamer labelNamer otlptranslator.LabelNamer @@ -48,20 +47,67 @@ type metricFamily struct { } func newCollector(config *Config, logger *zap.Logger) *collector { - labelNamer := otlptranslator.LabelNamer{} + labelNamer := configureLabelNamer(config) return &collector{ - accumulator: newAccumulator(logger, config.MetricExpiration), - logger: logger, - namespace: labelNamer.Build(config.Namespace), - sendTimestamps: config.SendTimestamps, - constLabels: config.ConstLabels, - addMetricSuffixes: config.AddMetricSuffixes, - metricExpiration: config.MetricExpiration, - metricNamer: otlptranslator.MetricNamer{WithMetricSuffixes: config.AddMetricSuffixes, Namespace: config.Namespace}, - labelNamer: labelNamer, + accumulator: newAccumulator(logger, config.MetricExpiration), + logger: logger, + namespace: labelNamer.Build(config.Namespace), + sendTimestamps: config.SendTimestamps, + constLabels: config.ConstLabels, + metricExpiration: config.MetricExpiration, + metricNamer: configureMetricNamer(config), + labelNamer: labelNamer, } } +// configureMetricNamer configures the MetricNamer based on the translation strategy or legacy configuration +func configureMetricNamer(config *Config) otlptranslator.MetricNamer { + withSuffixes, utf8Allowed := getTranslationConfiguration(config) + return otlptranslator.MetricNamer{ + WithMetricSuffixes: withSuffixes, + Namespace: config.Namespace, + UTF8Allowed: utf8Allowed, + } +} + +// configureLabelNamer configures the LabelNamer based on the translation strategy or legacy configuration +func configureLabelNamer(config *Config) otlptranslator.LabelNamer { + _, utf8Allowed := getTranslationConfiguration(config) + return otlptranslator.LabelNamer{ + UTF8Allowed: utf8Allowed, + } +} + +// getTranslationConfiguration returns the translation configuration based on the strategy or legacy settings +// Returns (withSuffixes, allowUTF8) +func getTranslationConfiguration(config *Config) (bool, bool) { + // If TranslationStrategy is explicitly set, use it (takes precedence) + if config.TranslationStrategy != "" { + switch config.TranslationStrategy { + case underscoreEscapingWithSuffixes: + return true, false + case underscoreEscapingWithoutSuffixes: + return false, false + case noUTF8EscapingWithSuffixes: + return true, true + case noTranslation: + return false, true + default: + // Fallback to default behavior, suffixes enabled, UTF-8 escaped to underscores. + return true, false + } + } + + // If feature gate is enabled, ignore AddMetricSuffixes (for deprecation) + if disableAddMetricSuffixesFeatureGate.IsEnabled() { + // Default to UnderscoreEscapingWithSuffixes behavior when AddMetricSuffixes is deprecated + return true, false + } + + // Fall back to legacy AddMetricSuffixes behavior, UTF-8 escaped to underscores. + return config.AddMetricSuffixes, false +} + func convertExemplars(exemplars pmetric.ExemplarSlice) []prometheus.Exemplar { length := exemplars.Len() result := make([]prometheus.Exemplar, length) diff --git a/exporter/prometheusexporter/config.go b/exporter/prometheusexporter/config.go index 61e3f77514e9c..7019e2f737195 100644 --- a/exporter/prometheusexporter/config.go +++ b/exporter/prometheusexporter/config.go @@ -4,11 +4,13 @@ package prometheusexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter" import ( + "fmt" "time" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/featuregate" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry" ) @@ -36,12 +38,49 @@ type Config struct { EnableOpenMetrics bool `mapstructure:"enable_open_metrics"` // AddMetricSuffixes controls whether suffixes are added to metric names. Defaults to true. + // Deprecated: Use TranslationStrategy instead. This setting is ignored when TranslationStrategy is explicitly set. AddMetricSuffixes bool `mapstructure:"add_metric_suffixes"` + + // TranslationStrategy controls how OTLP metric and attribute names are translated into Prometheus metric and label names. + // When set, this takes precedence over AddMetricSuffixes. + TranslationStrategy translationStrategy `mapstructure:"translation_strategy"` } var _ component.Config = (*Config)(nil) // Validate checks if the exporter configuration is valid -func (*Config) Validate() error { +func (cfg *Config) Validate() error { + // Validate translation strategy if set + if cfg.TranslationStrategy != "" { + switch cfg.TranslationStrategy { + case underscoreEscapingWithSuffixes, underscoreEscapingWithoutSuffixes, noUTF8EscapingWithSuffixes, noTranslation: + default: + return fmt.Errorf("invalid translation_strategy: %s", cfg.TranslationStrategy) + } + } return nil } + +type translationStrategy string + +const ( + // underscoreEscapingWithSuffixes fully escapes metric names for classic Prometheus metric name compatibility, + // and includes appending type and unit suffixes + underscoreEscapingWithSuffixes translationStrategy = "UnderscoreEscapingWithSuffixes" + + // underscoreEscapingWithoutSuffixes escapes special characters to '_', but suffixes won't be attached + underscoreEscapingWithoutSuffixes translationStrategy = "UnderscoreEscapingWithoutSuffixes" + + // noUTF8EscapingWithSuffixes disables changing special characters to '_'. Special suffixes like units and '_total' for counters will be attached + noUTF8EscapingWithSuffixes translationStrategy = "NoUTF8EscapingWithSuffixes" + + // noTranslation bypasses all metric and label name translation, passing them through unaltered + noTranslation translationStrategy = "NoTranslation" +) + +var disableAddMetricSuffixesFeatureGate = featuregate.GlobalRegistry().MustRegister( + "exporter.prometheusexporter.DisableAddMetricSuffixes", + featuregate.StageAlpha, + featuregate.WithRegisterDescription("When enabled, the deprecated add_metric_suffixes configuration option is ignored and translation_strategy is always used"), + featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-specification/pull/4533"), +) diff --git a/exporter/prometheusexporter/go.mod b/exporter/prometheusexporter/go.mod index e473ab031a2e2..06b7913a7b1e8 100644 --- a/exporter/prometheusexporter/go.mod +++ b/exporter/prometheusexporter/go.mod @@ -24,6 +24,7 @@ require ( go.opentelemetry.io/collector/consumer v1.37.0 go.opentelemetry.io/collector/exporter v0.131.0 go.opentelemetry.io/collector/exporter/exportertest v0.131.0 + go.opentelemetry.io/collector/featuregate v1.37.0 go.opentelemetry.io/collector/pdata v1.37.0 go.opentelemetry.io/collector/receiver/receivertest v0.131.0 go.opentelemetry.io/otel v1.37.0 @@ -206,7 +207,6 @@ require ( go.opentelemetry.io/collector/extension/extensionauth v1.37.0 // indirect go.opentelemetry.io/collector/extension/extensionmiddleware v0.131.0 // indirect go.opentelemetry.io/collector/extension/xextension v0.131.0 // indirect - go.opentelemetry.io/collector/featuregate v1.37.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.131.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.131.0 // indirect go.opentelemetry.io/collector/pdata/xpdata v0.131.0 // indirect diff --git a/exporter/prometheusexporter/prometheus_test.go b/exporter/prometheusexporter/prometheus_test.go index e40fa6f754f68..f9f759c2ac122 100644 --- a/exporter/prometheusexporter/prometheus_test.go +++ b/exporter/prometheusexporter/prometheus_test.go @@ -453,7 +453,7 @@ func metricBuilder(delta int64, prefix, job, instance string) pmetric.Metrics { m1 := ms.AppendEmpty() m1.SetName(prefix + "this/one/there(where)") m1.SetDescription("Extra ones") - m1.SetUnit("1") + m1.SetUnit("By") d1 := m1.SetEmptySum() d1.SetIsMonotonic(true) d1.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -467,7 +467,7 @@ func metricBuilder(delta int64, prefix, job, instance string) pmetric.Metrics { m2 := ms.AppendEmpty() m2.SetName(prefix + "this/one/there(where)") m2.SetDescription("Extra ones") - m2.SetUnit("1") + m2.SetUnit("By") d2 := m2.SetEmptySum() d2.SetIsMonotonic(true) d2.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -480,3 +480,220 @@ func metricBuilder(delta int64, prefix, job, instance string) pmetric.Metrics { return md } + +func TestPrometheusExporter_TranslationStrategies(t *testing.T) { + tests := []struct { + name string + featureGateEnabled bool + config *Config + extraHeaders map[string]string + want string + }{ + { + name: "Legacy AddMetricSuffixes=true (no translation_strategy set)", + featureGateEnabled: false, + config: &Config{ + AddMetricSuffixes: true, + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where_bytes_total Extra ones +# TYPE this_one_there_where_bytes_total counter +this_one_there_where_bytes_total{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where_bytes_total{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "Legacy AddMetricSuffixes=false (no translation_strategy set)", + featureGateEnabled: false, + config: &Config{ + AddMetricSuffixes: false, + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where Extra ones +# TYPE this_one_there_where counter +this_one_there_where{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "Legacy AddMetricSuffixes=true with feature gate enabled (no translation_strategy set)", + featureGateEnabled: true, + config: &Config{ + AddMetricSuffixes: true, // Should be ignored and default 'translation_strategy' is used (UnderscoreEscapingWithSuffixes). + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where_bytes_total Extra ones +# TYPE this_one_there_where_bytes_total counter +this_one_there_where_bytes_total{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where_bytes_total{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "TranslationStrategy takes precedence over AddMetricSuffixes (feature gate disabled)", + featureGateEnabled: false, + config: &Config{ + AddMetricSuffixes: true, // This should be ignored + TranslationStrategy: underscoreEscapingWithoutSuffixes, + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where Extra ones +# TYPE this_one_there_where counter +this_one_there_where{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "UnderscoreEscapingWithSuffixes", + config: &Config{ + TranslationStrategy: underscoreEscapingWithSuffixes, + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where_bytes_total Extra ones +# TYPE this_one_there_where_bytes_total counter +this_one_there_where_bytes_total{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where_bytes_total{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "UnderscoreEscapingWithoutSuffixes", + config: &Config{ + TranslationStrategy: underscoreEscapingWithoutSuffixes, + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where Extra ones +# TYPE this_one_there_where counter +this_one_there_where{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "NoUTF8EscapingWithSuffixes/escaping=allow-utf-8", + config: &Config{ + TranslationStrategy: noUTF8EscapingWithSuffixes, + }, + extraHeaders: map[string]string{ + "Accept": "application/openmetrics-text;version=1.0.0;escaping=allow-utf-8;q=0.6,application/openmetrics-text;version=0.0.1;q=0.5,text/plain;version=1.0.0;escaping=allow-utf-8;q=0.4,text/plain;version=0.0.4;q=0.3,*/*;q=0.2", + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP "this/one/there(where)_bytes_total" Extra ones +# TYPE "this/one/there(where)_bytes_total" counter +{"this/one/there(where)_bytes_total",arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +{"this/one/there(where)_bytes_total",arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "NoUTF8EscapingWithSuffixes/escaping=underscores", + config: &Config{ + TranslationStrategy: noUTF8EscapingWithSuffixes, + }, + extraHeaders: map[string]string{ + "Accept": "application/openmetrics-text;version=1.0.0;escaping=underscores;q=0.5,application/openmetrics-text;version=0.0.1;q=0.4,text/plain;version=1.0.0;escaping=underscores;q=0.3,text/plain;version=0.0.4;q=0.2,/;q=0.1", + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where__bytes_total Extra ones +# TYPE this_one_there_where__bytes_total counter +this_one_there_where__bytes_total{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where__bytes_total{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "NoTranslation/escaping=allow-utf-8", + featureGateEnabled: true, + config: &Config{ + TranslationStrategy: noTranslation, + }, + extraHeaders: map[string]string{ + "Accept": "application/openmetrics-text;version=1.0.0;escaping=allow-utf-8;q=0.6,application/openmetrics-text;version=0.0.1;q=0.5,text/plain;version=1.0.0;escaping=allow-utf-8;q=0.4,text/plain;version=0.0.4;q=0.3,*/*;q=0.2", + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP "this/one/there(where)" Extra ones +# TYPE "this/one/there(where)" counter +{"this/one/there(where)",arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +{"this/one/there(where)",arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + { + name: "NoTranslation/escaping=underscores", + config: &Config{ + TranslationStrategy: noTranslation, + }, + extraHeaders: map[string]string{ + "Accept": "application/openmetrics-text;version=1.0.0;escaping=underscores;q=0.5,application/openmetrics-text;version=0.0.1;q=0.4,text/plain;version=1.0.0;escaping=underscores;q=0.3,text/plain;version=0.0.4;q=0.2,/;q=0.1", + }, + want: `# HELP target_info Target metadata +# TYPE target_info gauge +target_info{instance="test-instance",job="test-service"} 1 +# HELP this_one_there_where_ Extra ones +# TYPE this_one_there_where_ counter +this_one_there_where_{arch="x86",instance="test-instance",job="test-service",os="linux",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 100 +this_one_there_where_{arch="x86",instance="test-instance",job="test-service",os="windows",otel_scope_name="",otel_scope_schema_url="",otel_scope_version=""} 99 +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set feature gate state for this test + originalState := disableAddMetricSuffixesFeatureGate.IsEnabled() + testutil.SetFeatureGateForTest(t, disableAddMetricSuffixesFeatureGate, tt.featureGateEnabled) + defer testutil.SetFeatureGateForTest(t, disableAddMetricSuffixesFeatureGate, originalState) + + // Configure the exporter + addr := testutil.GetAvailableLocalAddress(t) + cfg := tt.config + cfg.ServerConfig = confighttp.ServerConfig{ + Endpoint: addr, + } + cfg.MetricExpiration = 120 * time.Minute + + factory := NewFactory() + set := exportertest.NewNopSettings(metadata.Type) + exp, err := factory.CreateMetrics(context.Background(), set, cfg) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, exp.Shutdown(context.Background())) + }) + + assert.NotNil(t, exp) + require.NoError(t, exp.Start(context.Background(), componenttest.NewNopHost())) + + md := metricBuilder(0, "", "test-service", "test-instance") + assert.NoError(t, exp.ConsumeMetrics(context.Background(), md)) + + // Scrape metrics, with the Accept header set to the value specified in the test case + req, err := http.NewRequest(http.MethodGet, "http://"+addr+"/metrics", http.NoBody) + require.NoError(t, err) + for k, v := range tt.extraHeaders { + req.Header.Set(k, v) + } + res, err := http.DefaultClient.Do(req) + require.NoError(t, err, "Failed to perform a scrape") + assert.Equal(t, http.StatusOK, res.StatusCode, "Mismatched HTTP response status code") + + blob, _ := io.ReadAll(res.Body) + _ = res.Body.Close() + output := string(blob) + + assert.Equal(t, tt.want, output) + }) + } +}