-
Notifications
You must be signed in to change notification settings - Fork 5.8k
feat(outputs.opentelemetry): Support dot-separated OpenTelemetry metric names #18680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
15a3124
99c118f
b72ea8a
8b0a786
45d4f24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,8 +28,9 @@ var userAgent = internal.ProductToken() | |||||||||||||||||||||
| var sampleConfig string | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| type OpenTelemetry struct { | ||||||||||||||||||||||
| ServiceAddress string `toml:"service_address"` | ||||||||||||||||||||||
| EncodingType string `toml:"encoding_type"` | ||||||||||||||||||||||
| ServiceAddress string `toml:"service_address"` | ||||||||||||||||||||||
| EncodingType string `toml:"encoding_type"` | ||||||||||||||||||||||
| MetricNameFormat string `toml:"metric_name_format"` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| tls.ClientConfig | ||||||||||||||||||||||
| Timeout config.Duration `toml:"timeout"` | ||||||||||||||||||||||
|
|
@@ -82,6 +83,15 @@ func (o *OpenTelemetry) Connect() error { | |||||||||||||||||||||
| default: | ||||||||||||||||||||||
| return fmt.Errorf("invalid encoding %q", o.EncodingType) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if o.MetricNameFormat == "" { | ||||||||||||||||||||||
| o.MetricNameFormat = defaultMetricNameFormat | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| switch o.MetricNameFormat { | ||||||||||||||||||||||
| case "prometheus", "otel": | ||||||||||||||||||||||
|
Comment on lines
+86
to
+90
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| // Valid formats | ||||||||||||||||||||||
| default: | ||||||||||||||||||||||
| return fmt.Errorf("invalid metric_name_format %q, must be 'prometheus' or 'otel'", o.MetricNameFormat) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if o.Timeout <= 0 { | ||||||||||||||||||||||
| o.Timeout = defaultTimeout | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
@@ -178,7 +188,8 @@ func (o *OpenTelemetry) sendBatch(metrics []telegraf.Metric) error { | |||||||||||||||||||||
| o.Log.Warnf("Unrecognized metric type %v", metric.Type()) | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| err := batch.AddPoint(metric.Name(), metric.Tags(), metric.Fields(), metric.Time(), vType) | ||||||||||||||||||||||
| metricName := o.transformMetricName(metric.Name()) | ||||||||||||||||||||||
| err := batch.AddPoint(metricName, metric.Tags(), metric.Fields(), metric.Time(), vType) | ||||||||||||||||||||||
|
Comment on lines
+191
to
+192
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fold the code in here
Suggested change
|
||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| o.Log.Warnf("Failed to add point: %v", err) | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
|
|
@@ -208,18 +219,38 @@ func (o *OpenTelemetry) sendBatch(metrics []telegraf.Metric) error { | |||||||||||||||||||||
| return err | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // transformMetricName transforms metric names based on the configured format. | ||||||||||||||||||||||
| // For "otel" format, it preserves dots (OpenTelemetry semantic conventions). | ||||||||||||||||||||||
| // For "prometheus" format (default), it converts dots to underscores. | ||||||||||||||||||||||
| func (o *OpenTelemetry) transformMetricName(name string) string { | ||||||||||||||||||||||
| // If MetricNameFormat is not set, default to prometheus behavior for backward compatibility | ||||||||||||||||||||||
| format := o.MetricNameFormat | ||||||||||||||||||||||
| if format == "" { | ||||||||||||||||||||||
| format = defaultMetricNameFormat | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if format == "otel" { | ||||||||||||||||||||||
| // Preserve dots for OpenTelemetry semantic conventions | ||||||||||||||||||||||
| return name | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // Default "prometheus" format: convert dots to underscores | ||||||||||||||||||||||
| return strings.ReplaceAll(name, ".", "_") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const ( | ||||||||||||||||||||||
| defaultServiceAddress = "localhost:4317" | ||||||||||||||||||||||
| defaultTimeout = config.Duration(5 * time.Second) | ||||||||||||||||||||||
| defaultCompression = "gzip" | ||||||||||||||||||||||
| defaultServiceAddress = "localhost:4317" | ||||||||||||||||||||||
| defaultTimeout = config.Duration(5 * time.Second) | ||||||||||||||||||||||
| defaultCompression = "gzip" | ||||||||||||||||||||||
| defaultMetricNameFormat = "prometheus" | ||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please do not add constants for this! There is no benefit really! |
||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||
| outputs.Add("opentelemetry", func() telegraf.Output { | ||||||||||||||||||||||
| return &OpenTelemetry{ | ||||||||||||||||||||||
| ServiceAddress: defaultServiceAddress, | ||||||||||||||||||||||
| Timeout: defaultTimeout, | ||||||||||||||||||||||
| Compression: defaultCompression, | ||||||||||||||||||||||
| ServiceAddress: defaultServiceAddress, | ||||||||||||||||||||||
| Timeout: defaultTimeout, | ||||||||||||||||||||||
| Compression: defaultCompression, | ||||||||||||||||||||||
| MetricNameFormat: defaultMetricNameFormat, | ||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be set in |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -342,8 +342,230 @@ func (m *mockOtelService) Address() string { | |||||||||||
| func (m *mockOtelService) Export(ctx context.Context, request pmetricotlp.ExportRequest) (pmetricotlp.ExportResponse, error) { | ||||||||||||
| m.metrics = pmetric.NewMetrics() | ||||||||||||
| request.Metrics().CopyTo(m.metrics) | ||||||||||||
| // Only check metadata if it exists (for tests that provide headers) | ||||||||||||
| ctxMetadata, ok := metadata.FromIncomingContext(ctx) | ||||||||||||
| require.Equal(m.t, []string{"header1"}, ctxMetadata.Get("test")) | ||||||||||||
| require.True(m.t, ok) | ||||||||||||
| if ok { | ||||||||||||
| if testHeader := ctxMetadata.Get("test"); len(testHeader) > 0 { | ||||||||||||
| require.Equal(m.t, []string{"header1"}, testHeader) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+345
to
+351
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this change necessary? |
||||||||||||
| return pmetricotlp.NewExportResponse(), nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func TestOpenTelemetryMetricNameFormatPrometheus(t *testing.T) { | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about
Suggested change
|
||||||||||||
| expect := pmetric.NewMetrics() | ||||||||||||
| { | ||||||||||||
| rm := expect.ResourceMetrics().AppendEmpty() | ||||||||||||
| ilm := rm.ScopeMetrics().AppendEmpty() | ||||||||||||
| m := ilm.Metrics().AppendEmpty() | ||||||||||||
| m.SetName("http_server_duration") // dots converted to underscores | ||||||||||||
| m.SetEmptyGauge() | ||||||||||||
| dp := m.Gauge().DataPoints().AppendEmpty() | ||||||||||||
| dp.SetTimestamp(pcommon.Timestamp(1622848686000000000)) | ||||||||||||
| dp.SetDoubleValue(87.332) | ||||||||||||
| } | ||||||||||||
| m := newMockOtelService(t) | ||||||||||||
| t.Cleanup(m.Cleanup) | ||||||||||||
|
|
||||||||||||
| metricsConverter, err := influx2otel.NewLineProtocolToOtelMetrics(common.NoopLogger{}) | ||||||||||||
| require.NoError(t, err) | ||||||||||||
| plugin := &OpenTelemetry{ | ||||||||||||
|
Comment on lines
+371
to
+372
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| ServiceAddress: m.Address(), | ||||||||||||
| Timeout: config.Duration(time.Second), | ||||||||||||
| MetricNameFormat: "prometheus", | ||||||||||||
| metricsConverter: metricsConverter, | ||||||||||||
| otlpMetricClient: &gRPCClient{ | ||||||||||||
| grpcClientConn: m.GrpcClient(), | ||||||||||||
| metricsServiceClient: pmetricotlp.NewGRPCClient(m.GrpcClient()), | ||||||||||||
| }, | ||||||||||||
| Log: testutil.Logger{}, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| input := testutil.MustMetric( | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please avoid
Suggested change
Same for all other instances below. |
||||||||||||
| "http.server.duration", // dot-separated name | ||||||||||||
| map[string]string{}, | ||||||||||||
| map[string]interface{}{ | ||||||||||||
| "gauge": 87.332, | ||||||||||||
| }, | ||||||||||||
| time.Unix(0, 1622848686000000000), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| require.NoError(t, plugin.Write([]telegraf.Metric{input})) | ||||||||||||
|
|
||||||||||||
| got := m.GotMetrics() | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().Len()) | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().Len()) | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().Len()) | ||||||||||||
| require.Equal(t, "http_server_duration", got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name()) | ||||||||||||
|
Comment on lines
+395
to
+399
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All other tests compare the JSON representation of the metric. Can we please stick to this? Same for the other tests below. |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func TestOpenTelemetryMetricNameFormatOtel(t *testing.T) { | ||||||||||||
| expect := pmetric.NewMetrics() | ||||||||||||
| { | ||||||||||||
| rm := expect.ResourceMetrics().AppendEmpty() | ||||||||||||
| ilm := rm.ScopeMetrics().AppendEmpty() | ||||||||||||
| m := ilm.Metrics().AppendEmpty() | ||||||||||||
| m.SetName("http.server.duration") // dots preserved | ||||||||||||
| m.SetEmptyGauge() | ||||||||||||
| dp := m.Gauge().DataPoints().AppendEmpty() | ||||||||||||
| dp.SetTimestamp(pcommon.Timestamp(1622848686000000000)) | ||||||||||||
| dp.SetDoubleValue(87.332) | ||||||||||||
| } | ||||||||||||
| m := newMockOtelService(t) | ||||||||||||
| t.Cleanup(m.Cleanup) | ||||||||||||
|
|
||||||||||||
| metricsConverter, err := influx2otel.NewLineProtocolToOtelMetrics(common.NoopLogger{}) | ||||||||||||
| require.NoError(t, err) | ||||||||||||
| plugin := &OpenTelemetry{ | ||||||||||||
| ServiceAddress: m.Address(), | ||||||||||||
| Timeout: config.Duration(time.Second), | ||||||||||||
| MetricNameFormat: "otel", | ||||||||||||
| metricsConverter: metricsConverter, | ||||||||||||
| otlpMetricClient: &gRPCClient{ | ||||||||||||
| grpcClientConn: m.GrpcClient(), | ||||||||||||
| metricsServiceClient: pmetricotlp.NewGRPCClient(m.GrpcClient()), | ||||||||||||
| }, | ||||||||||||
| Log: testutil.Logger{}, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| input := testutil.MustMetric( | ||||||||||||
| "http.server.duration", // dot-separated name | ||||||||||||
| map[string]string{}, | ||||||||||||
| map[string]interface{}{ | ||||||||||||
| "gauge": 87.332, | ||||||||||||
| }, | ||||||||||||
| time.Unix(0, 1622848686000000000), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| require.NoError(t, plugin.Write([]telegraf.Metric{input})) | ||||||||||||
|
|
||||||||||||
| got := m.GotMetrics() | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().Len()) | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().Len()) | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().Len()) | ||||||||||||
| require.Equal(t, "http.server.duration", got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name()) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func TestOpenTelemetryMetricNameFormatDefault(t *testing.T) { | ||||||||||||
| expect := pmetric.NewMetrics() | ||||||||||||
| { | ||||||||||||
| rm := expect.ResourceMetrics().AppendEmpty() | ||||||||||||
| ilm := rm.ScopeMetrics().AppendEmpty() | ||||||||||||
| m := ilm.Metrics().AppendEmpty() | ||||||||||||
| m.SetName("http_server_duration") // default is prometheus format | ||||||||||||
| m.SetEmptyGauge() | ||||||||||||
| dp := m.Gauge().DataPoints().AppendEmpty() | ||||||||||||
| dp.SetTimestamp(pcommon.Timestamp(1622848686000000000)) | ||||||||||||
| dp.SetDoubleValue(87.332) | ||||||||||||
| } | ||||||||||||
| m := newMockOtelService(t) | ||||||||||||
| t.Cleanup(m.Cleanup) | ||||||||||||
|
|
||||||||||||
| metricsConverter, err := influx2otel.NewLineProtocolToOtelMetrics(common.NoopLogger{}) | ||||||||||||
| require.NoError(t, err) | ||||||||||||
| plugin := &OpenTelemetry{ | ||||||||||||
| ServiceAddress: m.Address(), | ||||||||||||
| Timeout: config.Duration(time.Second), | ||||||||||||
| MetricNameFormat: "", // empty should default to prometheus | ||||||||||||
| metricsConverter: metricsConverter, | ||||||||||||
| otlpMetricClient: &gRPCClient{ | ||||||||||||
| grpcClientConn: m.GrpcClient(), | ||||||||||||
| metricsServiceClient: pmetricotlp.NewGRPCClient(m.GrpcClient()), | ||||||||||||
| }, | ||||||||||||
| Log: testutil.Logger{}, | ||||||||||||
| } | ||||||||||||
| require.NoError(t, plugin.Connect()) // Connect sets default | ||||||||||||
|
|
||||||||||||
| input := testutil.MustMetric( | ||||||||||||
| "http.server.duration", // dot-separated name | ||||||||||||
| map[string]string{}, | ||||||||||||
| map[string]interface{}{ | ||||||||||||
| "gauge": 87.332, | ||||||||||||
| }, | ||||||||||||
| time.Unix(0, 1622848686000000000), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| require.NoError(t, plugin.Write([]telegraf.Metric{input})) | ||||||||||||
|
|
||||||||||||
| got := m.GotMetrics() | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().Len()) | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().Len()) | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().Len()) | ||||||||||||
| require.Equal(t, "http_server_duration", got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name()) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func TestOpenTelemetryInvalidMetricNameFormat(t *testing.T) { | ||||||||||||
| plugin := &OpenTelemetry{ | ||||||||||||
| ServiceAddress: "localhost:4317", | ||||||||||||
| MetricNameFormat: "invalid", | ||||||||||||
| Log: testutil.Logger{}, | ||||||||||||
| } | ||||||||||||
| err := plugin.Connect() | ||||||||||||
| require.Error(t, err) | ||||||||||||
| require.Contains(t, err.Error(), "invalid metric_name_format") | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func TestOpenTelemetryMetricNameWithUnderscores(t *testing.T) { | ||||||||||||
| // Test that existing underscores are preserved in both formats | ||||||||||||
| m := newMockOtelService(t) | ||||||||||||
| t.Cleanup(m.Cleanup) | ||||||||||||
|
|
||||||||||||
| metricsConverter, err := influx2otel.NewLineProtocolToOtelMetrics(common.NoopLogger{}) | ||||||||||||
| require.NoError(t, err) | ||||||||||||
|
|
||||||||||||
| // Test prometheus format - underscores should remain, dots should be converted | ||||||||||||
| plugin := &OpenTelemetry{ | ||||||||||||
| ServiceAddress: m.Address(), | ||||||||||||
| Timeout: config.Duration(time.Second), | ||||||||||||
| MetricNameFormat: "prometheus", | ||||||||||||
| metricsConverter: metricsConverter, | ||||||||||||
| otlpMetricClient: &gRPCClient{ | ||||||||||||
| grpcClientConn: m.GrpcClient(), | ||||||||||||
| metricsServiceClient: pmetricotlp.NewGRPCClient(m.GrpcClient()), | ||||||||||||
| }, | ||||||||||||
| Log: testutil.Logger{}, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| input := testutil.MustMetric( | ||||||||||||
| "http.server.request.duration", // dots and underscores | ||||||||||||
| map[string]string{}, | ||||||||||||
| map[string]interface{}{ | ||||||||||||
| "gauge": 87.332, | ||||||||||||
| }, | ||||||||||||
| time.Unix(0, 1622848686000000000), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| require.NoError(t, plugin.Write([]telegraf.Metric{input})) | ||||||||||||
| got := m.GotMetrics() | ||||||||||||
| require.Equal(t, 1, got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().Len()) | ||||||||||||
| // Dots should be converted to underscores | ||||||||||||
| require.Equal(t, "http_server_request_duration", got.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name()) | ||||||||||||
|
|
||||||||||||
| // Test otel format - everything should be preserved | ||||||||||||
| plugin2 := &OpenTelemetry{ | ||||||||||||
| ServiceAddress: m.Address(), | ||||||||||||
| Timeout: config.Duration(time.Second), | ||||||||||||
| MetricNameFormat: "otel", | ||||||||||||
| metricsConverter: metricsConverter, | ||||||||||||
| otlpMetricClient: &gRPCClient{ | ||||||||||||
| grpcClientConn: m.GrpcClient(), | ||||||||||||
| metricsServiceClient: pmetricotlp.NewGRPCClient(m.GrpcClient()), | ||||||||||||
| }, | ||||||||||||
| Log: testutil.Logger{}, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| input2 := testutil.MustMetric( | ||||||||||||
| "http.server.request.duration", | ||||||||||||
| map[string]string{}, | ||||||||||||
| map[string]interface{}{ | ||||||||||||
| "gauge": 87.332, | ||||||||||||
| }, | ||||||||||||
| time.Unix(0, 1622848686000000000), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| require.NoError(t, plugin2.Write([]telegraf.Metric{input2})) | ||||||||||||
| got2 := m.GotMetrics() | ||||||||||||
| require.Equal(t, 1, got2.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().Len()) | ||||||||||||
| // Dots should be preserved | ||||||||||||
| require.Equal(t, "http.server.request.duration", got2.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name()) | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,11 @@ | |||||||||||||
| ## protobuf, json | ||||||||||||||
| # encoding_type = "protobuf" | ||||||||||||||
|
|
||||||||||||||
| ## Override the default (prometheus) metric name format | ||||||||||||||
| ## prometheus: converts dots to underscores (default, Prometheus-compatible) | ||||||||||||||
| ## otel: preserves dot-separated names (OpenTelemetry semantic conventions) | ||||||||||||||
|
Comment on lines
+11
to
+13
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please be brief, defaults are documented in the option!
Suggested change
|
||||||||||||||
| # metric_name_format = "prometheus" | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the code before this PR, I don't see where we replace dots with underscores, so you are changing the default behavior and break everyone using this plugin. Am I missing anything? |
||||||||||||||
|
|
||||||||||||||
| ## Override the default (5s) request timeout | ||||||||||||||
| # timeout = "5s" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this change the existing behavior? In the existing code we do not replace anything in the metric name, do we?