Skip to content

Add WrapCollectorWith and WrapCollectorWithPrefix #1766

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

Merged
merged 1 commit into from
Apr 10, 2025
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
80 changes: 80 additions & 0 deletions prometheus/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/prometheus/common/expfmt"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

Expand Down Expand Up @@ -784,3 +785,82 @@ func ExampleCollectorFunc() {
// Output:
// {"name":"http_requests_info","help":"Information about the received HTTP requests.","type":"COUNTER","metric":[{"label":[{"name":"code","value":"200"},{"name":"method","value":"GET"}],"counter":{"value":42}},{"label":[{"name":"code","value":"404"},{"name":"method","value":"POST"}],"counter":{"value":15}}]}
}

// Using WrapCollectorWith to un-register metrics registered by a third party lib.
// newThirdPartyLibFoo illustrates a constructor from a third-party lib that does
// not expose any way to un-register metrics.
func ExampleWrapCollectorWith() {
reg := prometheus.NewRegistry()

// We want to create two instances of thirdPartyLibFoo, each one wrapped with
// its "instance" label.
firstReg := prometheus.NewRegistry()
_ = newThirdPartyLibFoo(firstReg)
firstCollector := prometheus.WrapCollectorWith(prometheus.Labels{"instance": "first"}, firstReg)
reg.MustRegister(firstCollector)

secondReg := prometheus.NewRegistry()
_ = newThirdPartyLibFoo(secondReg)
secondCollector := prometheus.WrapCollectorWith(prometheus.Labels{"instance": "second"}, secondReg)
reg.MustRegister(secondCollector)

// So far we have illustrated that we can create two instances of thirdPartyLibFoo,
// wrapping each one's metrics with some const label.
// This is something we could've achieved by doing:
// newThirdPartyLibFoo(prometheus.WrapRegistererWith(prometheus.Labels{"instance": "first"}, reg))
metricFamilies, err := reg.Gather()
if err != nil {
panic("unexpected behavior of registry")
}
fmt.Println("Both instances:")
fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0])))

// Now we want to unregister first Foo's metrics, and then register them again.
// This is not possible by passing a wrapped Registerer to newThirdPartyLibFoo,
// because we have already lost track of the registered Collectors,
// however since we've collected Foo's metrics in it's own Registry, and we have registered that
// as a specific Collector, we can now de-register them:
unregistered := reg.Unregister(firstCollector)
if !unregistered {
panic("unexpected behavior of registry")
}

metricFamilies, err = reg.Gather()
if err != nil {
panic("unexpected behavior of registry")
}
fmt.Println("First unregistered:")
fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0])))

// Now we can create another instance of Foo with {instance: "first"} label again.
firstRegAgain := prometheus.NewRegistry()
_ = newThirdPartyLibFoo(firstRegAgain)
firstCollectorAgain := prometheus.WrapCollectorWith(prometheus.Labels{"instance": "first"}, firstRegAgain)
reg.MustRegister(firstCollectorAgain)

metricFamilies, err = reg.Gather()
if err != nil {
panic("unexpected behavior of registry")
}
fmt.Println("Both again:")
fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0])))

// Output:
// Both instances:
// {"name":"foo","help":"Registered forever.","type":"GAUGE","metric":[{"label":[{"name":"instance","value":"first"}],"gauge":{"value":1}},{"label":[{"name":"instance","value":"second"}],"gauge":{"value":1}}]}
// First unregistered:
// {"name":"foo","help":"Registered forever.","type":"GAUGE","metric":[{"label":[{"name":"instance","value":"second"}],"gauge":{"value":1}}]}
// Both again:
// {"name":"foo","help":"Registered forever.","type":"GAUGE","metric":[{"label":[{"name":"instance","value":"first"}],"gauge":{"value":1}},{"label":[{"name":"instance","value":"second"}],"gauge":{"value":1}}]}
}

func newThirdPartyLibFoo(reg prometheus.Registerer) struct{} {
foo := struct{}{}
// Register the metrics of the third party lib.
c := promauto.With(reg).NewGauge(prometheus.GaugeOpts{
Name: "foo",
Help: "Registered forever.",
})
c.Set(1)
return foo
}
36 changes: 35 additions & 1 deletion prometheus/wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func WrapRegistererWith(labels Labels, reg Registerer) Registerer {
// metric names that are standardized across applications, as that would break
// horizontal monitoring, for example the metrics provided by the Go collector
// (see NewGoCollector) and the process collector (see NewProcessCollector). (In
// fact, those metrics are already prefixed with go_ or process_,
// fact, those metrics are already prefixed with "go_" or "process_",
// respectively.)
//
// Conflicts between Collectors registered through the original Registerer with
Expand All @@ -78,6 +78,40 @@ func WrapRegistererWithPrefix(prefix string, reg Registerer) Registerer {
}
}

// WrapCollectorWith returns a Collector wrapping the provided Collector. The
// wrapped Collector will add the provided Labels to all Metrics it collects (as
// ConstLabels). The Metrics collected by the unmodified Collector must not
// duplicate any of those labels.
//
// WrapCollectorWith can be useful to work with multiple instances of a third
// party library that does not expose enough flexibility on the lifecycle of its
// registered metrics.
// For example, let's say you have a foo.New(reg Registerer) constructor that
// registers metrics but never unregisters them, and you want to create multiple
// instances of foo.Foo with different labels.
// The way to achieve that, is to create a new Registry, pass it to foo.New,
// then use WrapCollectorWith to wrap that Registry with the desired labels and
// register that as a collector in your main Registry.
// Then you can un-register the wrapped collector effectively un-registering the
// metrics registered by foo.New.
func WrapCollectorWith(labels Labels, c Collector) Collector {
return &wrappingCollector{
wrappedCollector: c,
labels: labels,
}
}

// WrapCollectorWithPrefix returns a Collector wrapping the provided Collector. The
// wrapped Collector will add the provided prefix to the name of all Metrics it collects.
//
// See the documentation of WrapCollectorWith for more details on the use case.
func WrapCollectorWithPrefix(prefix string, c Collector) Collector {
return &wrappingCollector{
wrappedCollector: c,
prefix: prefix,
}
}

type wrappingRegisterer struct {
wrappedRegisterer Registerer
prefix string
Expand Down
169 changes: 148 additions & 21 deletions prometheus/wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func toMetricFamilies(cs ...Collector) []*dto.MetricFamily {
return out
}

func TestWrap(t *testing.T) {
func TestWrapRegisterer(t *testing.T) {
now := time.Now()
nowFn := func() time.Time { return now }
simpleCnt := NewCounter(CounterOpts{
Expand Down Expand Up @@ -306,28 +306,34 @@ func TestWrap(t *testing.T) {
if !s.gatherFails && err != nil {
t.Fatal("gathering failed:", err)
}
if len(wantMF) != len(gotMF) {
t.Fatalf("Expected %d metricFamilies, got %d", len(wantMF), len(gotMF))
assertEqualMFs(t, wantMF, gotMF)
})
}
}

func assertEqualMFs(t *testing.T, wantMF, gotMF []*dto.MetricFamily) {
t.Helper()

if len(wantMF) != len(gotMF) {
t.Fatalf("Expected %d metricFamilies, got %d", len(wantMF), len(gotMF))
}
for i := range gotMF {
if !proto.Equal(gotMF[i], wantMF[i]) {
var want, got []string

for i, mf := range wantMF {
want = append(want, fmt.Sprintf("%3d: %s", i, mf))
}
for i := range gotMF {
if !proto.Equal(gotMF[i], wantMF[i]) {
var want, got []string

for i, mf := range wantMF {
want = append(want, fmt.Sprintf("%3d: %s", i, mf))
}
for i, mf := range gotMF {
got = append(got, fmt.Sprintf("%3d: %s", i, mf))
}

t.Fatalf(
"unexpected output of gathering:\n\nWANT:\n%s\n\nGOT:\n%s\n",
strings.Join(want, "\n"),
strings.Join(got, "\n"),
)
}
for i, mf := range gotMF {
got = append(got, fmt.Sprintf("%3d: %s", i, mf))
}
})

t.Fatalf(
"unexpected output of gathering:\n\nWANT:\n%s\n\nGOT:\n%s\n",
strings.Join(want, "\n"),
strings.Join(got, "\n"),
)
}
}
}

Expand All @@ -339,3 +345,124 @@ func TestNil(t *testing.T) {
t.Fatal("registering failed:", err)
}
}

func TestWrapCollector(t *testing.T) {
t.Run("can be registered and un-registered", func(t *testing.T) {
inner := NewPedanticRegistry()
g := NewGauge(GaugeOpts{Name: "testing"})
g.Set(42)
err := inner.Register(g)
if err != nil {
t.Fatal("registering failed:", err)
}

wrappedWithLabels := WrapCollectorWith(Labels{"lbl": "1"}, inner)
wrappedWithPrefix := WrapCollectorWithPrefix("prefix", inner)
reg := NewPedanticRegistry()
err = reg.Register(wrappedWithLabels)
if err != nil {
t.Fatal("registering failed:", err)
}
err = reg.Register(wrappedWithPrefix)
if err != nil {
t.Fatal("registering failed:", err)
}

gathered, err := reg.Gather()
if err != nil {
t.Fatal("gathering failed:", err)
}

lg := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "1"}})
lg.Set(42)
pg := NewGauge(GaugeOpts{Name: "prefixtesting"})
pg.Set(42)
expected := toMetricFamilies(lg, pg)
assertEqualMFs(t, expected, gathered)

if !reg.Unregister(wrappedWithLabels) {
t.Fatal("unregistering failed")
}
if !reg.Unregister(wrappedWithPrefix) {
t.Fatal("unregistering failed")
}

gathered, err = reg.Gather()
if err != nil {
t.Fatal("gathering failed:", err)
}
if len(gathered) != 0 {
t.Fatalf("expected 0 metric families, got %d", len(gathered))
}
})

t.Run("can wrap same collector twice", func(t *testing.T) {
inner := NewPedanticRegistry()
g := NewGauge(GaugeOpts{Name: "testing"})
g.Set(42)
err := inner.Register(g)
if err != nil {
t.Fatal("registering failed:", err)
}

wrapped := WrapCollectorWith(Labels{"lbl": "1"}, inner)
reg := NewPedanticRegistry()
err = reg.Register(wrapped)
if err != nil {
t.Fatal("registering failed:", err)
}

wrapped2 := WrapCollectorWith(Labels{"lbl": "2"}, inner)
err = reg.Register(wrapped2)
if err != nil {
t.Fatal("registering failed:", err)
}

gathered, err := reg.Gather()
if err != nil {
t.Fatal("gathering failed:", err)
}

lg := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "1"}})
lg.Set(42)
lg2 := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "2"}})
lg2.Set(42)
expected := toMetricFamilies(lg, lg2)
assertEqualMFs(t, expected, gathered)
})

t.Run("can be registered again after un-registering", func(t *testing.T) {
inner := NewPedanticRegistry()
g := NewGauge(GaugeOpts{Name: "testing"})
g.Set(42)
err := inner.Register(g)
if err != nil {
t.Fatal("registering failed:", err)
}

wrapped := WrapCollectorWith(Labels{"lbl": "1"}, inner)
reg := NewPedanticRegistry()
err = reg.Register(wrapped)
if err != nil {
t.Fatal("registering failed:", err)
}

if !reg.Unregister(wrapped) {
t.Fatal("unregistering failed")
}
err = reg.Register(wrapped)
if err != nil {
t.Fatal("registering failed:", err)
}

gathered, err := reg.Gather()
if err != nil {
t.Fatal("gathering failed:", err)
}

lg := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "1"}})
lg.Set(42)
expected := toMetricFamilies(lg)
assertEqualMFs(t, expected, gathered)
})
}
Loading