Skip to content
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

prometheus: add WithNamespace option to prefix metrics #3970

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions exporters/prometheus/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,6 +29,7 @@ type config struct {
withoutUnits bool
aggregation metric.AggregationSelector
disableScopeInfo bool
namespace string
}

// newConfig creates a validated config configured with options.
Expand Down Expand Up @@ -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
})
}
30 changes: 30 additions & 0 deletions exporters/prometheus/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
if c.withoutUnits {
return name
}
Expand Down
20 changes: 20 additions & 0 deletions exporters/prometheus/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions exporters/prometheus/testdata/with_namespace.txt
Original file line number Diff line number Diff line change
@@ -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