mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-01-10 00:29:12 +02:00
prometheus: add WithNamespace option to prefix metrics (#3970)
* prometheus: add WithNamespace option to prefix metrics * sanitize namespace, add config test * Update CHANGELOG.md Co-authored-by: Robert Pająk <pellared@hotmail.com> --------- Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
parent
02fa1e2a8d
commit
1c05d9c0b7
@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
- The `go.opentelemetry.io/otel/metric/embedded` package. (#3916)
|
||||
- The `Version` function to `go.opentelemetry.io/otel/sdk` to return the SDK version. (#3949)
|
||||
- Add a `WithNamespace` option to `go.opentelemetry.io/otel/exporters/prometheus` to allow users to prefix metrics with a namespace. (#3970)
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
||||
package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
@ -27,6 +29,7 @@ type config struct {
|
||||
withoutUnits bool
|
||||
aggregation metric.AggregationSelector
|
||||
disableScopeInfo bool
|
||||
namespace string
|
||||
}
|
||||
|
||||
// newConfig creates a validated config configured with options.
|
||||
@ -116,3 +119,20 @@ func WithoutScopeInfo() Option {
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
||||
// WithNamespace configures the Exporter to prefix metric with the given namespace.
|
||||
// Metadata metrics such as target_info and otel_scope_info are not prefixed since these
|
||||
// have special behavior based on their name.
|
||||
func WithNamespace(ns string) Option {
|
||||
return optionFunc(func(cfg config) config {
|
||||
ns = sanitizeName(ns)
|
||||
if !strings.HasSuffix(ns, "_") {
|
||||
// namespace and metric names should be separated with an underscore,
|
||||
// adds a trailing underscore if there is not one already.
|
||||
ns = ns + "_"
|
||||
}
|
||||
|
||||
cfg.namespace = ns
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
@ -99,6 +99,36 @@ func TestNewConfig(t *testing.T) {
|
||||
withoutUnits: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with namespace",
|
||||
options: []Option{
|
||||
WithNamespace("test"),
|
||||
},
|
||||
wantConfig: config{
|
||||
registerer: prometheus.DefaultRegisterer,
|
||||
namespace: "test_",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with namespace with trailing underscore",
|
||||
options: []Option{
|
||||
WithNamespace("test_"),
|
||||
},
|
||||
wantConfig: config{
|
||||
registerer: prometheus.DefaultRegisterer,
|
||||
namespace: "test_",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with unsanitized namespace",
|
||||
options: []Option{
|
||||
WithNamespace("test/"),
|
||||
},
|
||||
wantConfig: config{
|
||||
registerer: prometheus.DefaultRegisterer,
|
||||
namespace: "test_",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -66,6 +66,7 @@ type collector struct {
|
||||
createTargetInfoOnce sync.Once
|
||||
scopeInfos map[instrumentation.Scope]prometheus.Metric
|
||||
metricFamilies map[string]*dto.MetricFamily
|
||||
namespace string
|
||||
}
|
||||
|
||||
// prometheus counters MUST have a _total suffix:
|
||||
@ -88,6 +89,7 @@ func New(opts ...Option) (*Exporter, error) {
|
||||
disableScopeInfo: cfg.disableScopeInfo,
|
||||
scopeInfos: make(map[instrumentation.Scope]prometheus.Metric),
|
||||
metricFamilies: make(map[string]*dto.MetricFamily),
|
||||
namespace: cfg.namespace,
|
||||
}
|
||||
|
||||
if err := cfg.registerer.Register(collector); err != nil {
|
||||
@ -316,9 +318,12 @@ var unitSuffixes = map[string]string{
|
||||
"ms": "_milliseconds",
|
||||
}
|
||||
|
||||
// getName returns the sanitized name, including unit suffix.
|
||||
// getName returns the sanitized name, prefixed with the namespace and suffixed with unit.
|
||||
func (c *collector) getName(m metricdata.Metrics) string {
|
||||
name := sanitizeName(m.Name)
|
||||
if c.namespace != "" {
|
||||
name = c.namespace + name
|
||||
}
|
||||
if c.withoutUnits {
|
||||
return name
|
||||
}
|
||||
|
@ -258,6 +258,26 @@ func TestPrometheusExporter(t *testing.T) {
|
||||
counter.Add(ctx, 1, attrs...)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with namespace",
|
||||
expectedFile: "testdata/with_namespace.txt",
|
||||
options: []Option{
|
||||
WithNamespace("test"),
|
||||
},
|
||||
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.Key("A").String("B"),
|
||||
attribute.Key("C").String("D"),
|
||||
attribute.Key("E").Bool(true),
|
||||
attribute.Key("F").Int(42),
|
||||
}
|
||||
counter, err := meter.Float64Counter("foo", instrument.WithDescription("a simple counter"))
|
||||
require.NoError(t, err)
|
||||
counter.Add(ctx, 5, attrs...)
|
||||
counter.Add(ctx, 10.3, attrs...)
|
||||
counter.Add(ctx, 9, attrs...)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
9
exporters/prometheus/testdata/with_namespace.txt
vendored
Executable file
9
exporters/prometheus/testdata/with_namespace.txt
vendored
Executable file
@ -0,0 +1,9 @@
|
||||
# HELP test_foo_total a simple counter
|
||||
# TYPE test_foo_total counter
|
||||
test_foo_total{A="B",C="D",E="true",F="42",otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 24.3
|
||||
# HELP otel_scope_info Instrumentation Scope metadata
|
||||
# TYPE otel_scope_info gauge
|
||||
otel_scope_info{otel_scope_name="testmeter",otel_scope_version="v0.1.0"} 1
|
||||
# HELP target_info Target metadata
|
||||
# TYPE target_info gauge
|
||||
target_info{service_name="prometheus_test",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="latest"} 1
|
Loading…
Reference in New Issue
Block a user