Skip to content

Commit 56d1efd

Browse files
committed
Add WrapCollectorWith and WrapCollectorWithPrefix
I found myself in a situation where I had to instantiate multiple instances of a third party library that registered some metrics and never un-registered them. In my app's lifecycle I needed to un-register them and then register them again, but I couldn't achieve that by wrapping the Registerer. I decided to register that library's metrics in a separate Registry and register that Registry, but in order to handle multiple instances of the library, I also needed to wrap it with labels, and while such functionality existed already as part of the Registerer wrapping, it wasn't exposed. This PR just exposes Collector wrapping as a public function. Signed-off-by: Oleg Zaytsev <[email protected]>
1 parent f2276aa commit 56d1efd

File tree

3 files changed

+264
-22
lines changed

3 files changed

+264
-22
lines changed

prometheus/examples_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/prometheus/common/expfmt"
2828

2929
"github.com/prometheus/client_golang/prometheus"
30+
"github.com/prometheus/client_golang/prometheus/promauto"
3031
"github.com/prometheus/client_golang/prometheus/promhttp"
3132
)
3233

@@ -784,3 +785,83 @@ func ExampleCollectorFunc() {
784785
// Output:
785786
// {"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}}]}
786787
}
788+
789+
// Using WrapCollectorWith to un-register metrics registered by a third party lib.
790+
// newThirdPartyLibFoo illustrates a constructor from a third-party lib that does
791+
// not expose any way to un-register metrics.
792+
func ExampleWrapCollectorWith() {
793+
reg := prometheus.NewRegistry()
794+
795+
// We want to create two instances of thirdPartyLibFoo, each one wrapped with
796+
// its "instance" label.
797+
firstReg := prometheus.NewRegistry()
798+
_ = newThirdPartyLibFoo(firstReg)
799+
firstCollector := prometheus.WrapCollectorWith(prometheus.Labels{"instance": "first"}, firstReg)
800+
reg.MustRegister(firstCollector)
801+
802+
secondReg := prometheus.NewRegistry()
803+
_ = newThirdPartyLibFoo(secondReg)
804+
secondCollector := prometheus.WrapCollectorWith(prometheus.Labels{"instance": "second"}, secondReg)
805+
reg.MustRegister(secondCollector)
806+
807+
// So far we have illustrated that we can create two instances of thirdPartyLibFoo,
808+
// wrapping each one's metrics with some const label.
809+
// This is something we could've achieved by doing:
810+
// newThirdPartyLibFoo(prometheus.WrapRegistererWith(prometheus.Labels{"instance": "first"}, reg))
811+
metricFamilies, err := reg.Gather()
812+
if err != nil {
813+
panic("unexpected behavior of registry")
814+
}
815+
fmt.Println("Both instances:")
816+
fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0])))
817+
818+
// Now we want to unregister first Foo's metrics, and then register them again.
819+
// This is not possible by passing a wrapped Registerer to newThirdPartyLibFoo,
820+
// because we have already lost track of the registered Collectors,
821+
// however since we've collected Foo's metrics in it's own Registry, and we have registered that
822+
// as a specific Collector, we can now de-register them:
823+
unregistered := reg.Unregister(firstCollector)
824+
if !unregistered {
825+
panic("unexpected behavior of registry")
826+
}
827+
828+
metricFamilies, err = reg.Gather()
829+
if err != nil {
830+
panic("unexpected behavior of registry")
831+
}
832+
fmt.Println("First unregistered:")
833+
fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0])))
834+
835+
// Now we can create another instance of Foo with {instance: "first"} label again.
836+
firstRegAgain := prometheus.NewRegistry()
837+
_ = newThirdPartyLibFoo(firstRegAgain)
838+
firstCollectorAgain := prometheus.WrapCollectorWith(prometheus.Labels{"instance": "first"}, firstRegAgain)
839+
reg.MustRegister(firstCollectorAgain)
840+
841+
metricFamilies, err = reg.Gather()
842+
if err != nil {
843+
panic("unexpected behavior of registry")
844+
}
845+
fmt.Println("Both again:")
846+
fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0])))
847+
848+
// Output:
849+
// Both instances:
850+
// {"name":"foo","help":"Registered forever.","type":"GAUGE","metric":[{"label":[{"name":"instance","value":"first"}],"gauge":{"value":1}},{"label":[{"name":"instance","value":"second"}],"gauge":{"value":1}}]}
851+
// First unregistered:
852+
// {"name":"foo","help":"Registered forever.","type":"GAUGE","metric":[{"label":[{"name":"instance","value":"second"}],"gauge":{"value":1}}]}
853+
// Both again:
854+
// {"name":"foo","help":"Registered forever.","type":"GAUGE","metric":[{"label":[{"name":"instance","value":"first"}],"gauge":{"value":1}},{"label":[{"name":"instance","value":"second"}],"gauge":{"value":1}}]}
855+
856+
}
857+
858+
func newThirdPartyLibFoo(reg prometheus.Registerer) struct{} {
859+
foo := struct{}{}
860+
// Register the metrics of the third party lib.
861+
c := promauto.With(reg).NewGauge(prometheus.GaugeOpts{
862+
Name: "foo",
863+
Help: "Registered forever.",
864+
})
865+
c.Set(1)
866+
return foo
867+
}

prometheus/wrap.go

+35-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func WrapRegistererWith(labels Labels, reg Registerer) Registerer {
6363
// metric names that are standardized across applications, as that would break
6464
// horizontal monitoring, for example the metrics provided by the Go collector
6565
// (see NewGoCollector) and the process collector (see NewProcessCollector). (In
66-
// fact, those metrics are already prefixed with go_ or process_,
66+
// fact, those metrics are already prefixed with "go_" or "process_",
6767
// respectively.)
6868
//
6969
// Conflicts between Collectors registered through the original Registerer with
@@ -78,6 +78,40 @@ func WrapRegistererWithPrefix(prefix string, reg Registerer) Registerer {
7878
}
7979
}
8080

81+
// WrapCollectorWith returns a Collector wrapping the provided Collector. The
82+
// wrapped Collector will add the provided Labels to all Metrics it collects (as
83+
// ConstLabels). The Metrics collected by the unmodified Collector must not
84+
// duplicate any of those labels.
85+
//
86+
// WrapCollectorWith can be useful to work with multiple instances of a third
87+
// party library that does not expose enough flexibility on the lifecycle of its
88+
// registered metrics.
89+
// For example, let's say you have a foo.New(reg Registerer) constructor that
90+
// registers metrics but never unregisters them, and you want to create multiple
91+
// instances of foo.Foo with different labels.
92+
// The way to achieve that, is to create a new Registry, pass it to foo.New,
93+
// then use WrapCollectorWith to wrap that Registry with the desired labels and
94+
// register that as a collector in your main Registry.
95+
// Then you can un-register the wrapped collector effectively un-registering the
96+
// metrics registered by foo.New.
97+
func WrapCollectorWith(labels Labels, c Collector) Collector {
98+
return &wrappingCollector{
99+
wrappedCollector: c,
100+
labels: labels,
101+
}
102+
}
103+
104+
// WrapCollectorWithPrefix returns a Collector wrapping the provided Collector. The
105+
// wrapped Collector will add the provided prefix to the name of all Metrics it collects.
106+
//
107+
// See the documentation of WrapCollectorWith for more details on the use case.
108+
func WrapCollectorWithPrefix(prefix string, c Collector) Collector {
109+
return &wrappingCollector{
110+
wrappedCollector: c,
111+
prefix: prefix,
112+
}
113+
}
114+
81115
type wrappingRegisterer struct {
82116
wrappedRegisterer Registerer
83117
prefix string

prometheus/wrap_test.go

+148-21
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func toMetricFamilies(cs ...Collector) []*dto.MetricFamily {
4343
return out
4444
}
4545

46-
func TestWrap(t *testing.T) {
46+
func TestWrapRegisterer(t *testing.T) {
4747
now := time.Now()
4848
nowFn := func() time.Time { return now }
4949
simpleCnt := NewCounter(CounterOpts{
@@ -306,28 +306,34 @@ func TestWrap(t *testing.T) {
306306
if !s.gatherFails && err != nil {
307307
t.Fatal("gathering failed:", err)
308308
}
309-
if len(wantMF) != len(gotMF) {
310-
t.Fatalf("Expected %d metricFamilies, got %d", len(wantMF), len(gotMF))
309+
assertEqualMFs(t, wantMF, gotMF)
310+
})
311+
}
312+
}
313+
314+
func assertEqualMFs(t *testing.T, wantMF, gotMF []*dto.MetricFamily) {
315+
t.Helper()
316+
317+
if len(wantMF) != len(gotMF) {
318+
t.Fatalf("Expected %d metricFamilies, got %d", len(wantMF), len(gotMF))
319+
}
320+
for i := range gotMF {
321+
if !proto.Equal(gotMF[i], wantMF[i]) {
322+
var want, got []string
323+
324+
for i, mf := range wantMF {
325+
want = append(want, fmt.Sprintf("%3d: %s", i, mf))
311326
}
312-
for i := range gotMF {
313-
if !proto.Equal(gotMF[i], wantMF[i]) {
314-
var want, got []string
315-
316-
for i, mf := range wantMF {
317-
want = append(want, fmt.Sprintf("%3d: %s", i, mf))
318-
}
319-
for i, mf := range gotMF {
320-
got = append(got, fmt.Sprintf("%3d: %s", i, mf))
321-
}
322-
323-
t.Fatalf(
324-
"unexpected output of gathering:\n\nWANT:\n%s\n\nGOT:\n%s\n",
325-
strings.Join(want, "\n"),
326-
strings.Join(got, "\n"),
327-
)
328-
}
327+
for i, mf := range gotMF {
328+
got = append(got, fmt.Sprintf("%3d: %s", i, mf))
329329
}
330-
})
330+
331+
t.Fatalf(
332+
"unexpected output of gathering:\n\nWANT:\n%s\n\nGOT:\n%s\n",
333+
strings.Join(want, "\n"),
334+
strings.Join(got, "\n"),
335+
)
336+
}
331337
}
332338
}
333339

@@ -339,3 +345,124 @@ func TestNil(t *testing.T) {
339345
t.Fatal("registering failed:", err)
340346
}
341347
}
348+
349+
func TestWrapCollector(t *testing.T) {
350+
t.Run("can be registered and un-registered", func(t *testing.T) {
351+
inner := NewPedanticRegistry()
352+
g := NewGauge(GaugeOpts{Name: "testing"})
353+
g.Set(42)
354+
err := inner.Register(g)
355+
if err != nil {
356+
t.Fatal("registering failed:", err)
357+
}
358+
359+
wrappedWithLabels := WrapCollectorWith(Labels{"lbl": "1"}, inner)
360+
wrappedWithPrefix := WrapCollectorWithPrefix("prefix", inner)
361+
reg := NewPedanticRegistry()
362+
err = reg.Register(wrappedWithLabels)
363+
if err != nil {
364+
t.Fatal("registering failed:", err)
365+
}
366+
err = reg.Register(wrappedWithPrefix)
367+
if err != nil {
368+
t.Fatal("registering failed:", err)
369+
}
370+
371+
gathered, err := reg.Gather()
372+
if err != nil {
373+
t.Fatal("gathering failed:", err)
374+
}
375+
376+
lg := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "1"}})
377+
lg.Set(42)
378+
pg := NewGauge(GaugeOpts{Name: "prefixtesting"})
379+
pg.Set(42)
380+
expected := toMetricFamilies(lg, pg)
381+
assertEqualMFs(t, expected, gathered)
382+
383+
if !reg.Unregister(wrappedWithLabels) {
384+
t.Fatal("unregistering failed")
385+
}
386+
if !reg.Unregister(wrappedWithPrefix) {
387+
t.Fatal("unregistering failed")
388+
}
389+
390+
gathered, err = reg.Gather()
391+
if err != nil {
392+
t.Fatal("gathering failed:", err)
393+
}
394+
if len(gathered) != 0 {
395+
t.Fatalf("expected 0 metric families, got %d", len(gathered))
396+
}
397+
})
398+
399+
t.Run("can wrap same collector twice", func(t *testing.T) {
400+
inner := NewPedanticRegistry()
401+
g := NewGauge(GaugeOpts{Name: "testing"})
402+
g.Set(42)
403+
err := inner.Register(g)
404+
if err != nil {
405+
t.Fatal("registering failed:", err)
406+
}
407+
408+
wrapped := WrapCollectorWith(Labels{"lbl": "1"}, inner)
409+
reg := NewPedanticRegistry()
410+
err = reg.Register(wrapped)
411+
if err != nil {
412+
t.Fatal("registering failed:", err)
413+
}
414+
415+
wrapped2 := WrapCollectorWith(Labels{"lbl": "2"}, inner)
416+
err = reg.Register(wrapped2)
417+
if err != nil {
418+
t.Fatal("registering failed:", err)
419+
}
420+
421+
gathered, err := reg.Gather()
422+
if err != nil {
423+
t.Fatal("gathering failed:", err)
424+
}
425+
426+
lg := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "1"}})
427+
lg.Set(42)
428+
lg2 := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "2"}})
429+
lg2.Set(42)
430+
expected := toMetricFamilies(lg, lg2)
431+
assertEqualMFs(t, expected, gathered)
432+
})
433+
434+
t.Run("can be registered again after un-registering", func(t *testing.T) {
435+
inner := NewPedanticRegistry()
436+
g := NewGauge(GaugeOpts{Name: "testing"})
437+
g.Set(42)
438+
err := inner.Register(g)
439+
if err != nil {
440+
t.Fatal("registering failed:", err)
441+
}
442+
443+
wrapped := WrapCollectorWith(Labels{"lbl": "1"}, inner)
444+
reg := NewPedanticRegistry()
445+
err = reg.Register(wrapped)
446+
if err != nil {
447+
t.Fatal("registering failed:", err)
448+
}
449+
450+
if !reg.Unregister(wrapped) {
451+
t.Fatal("unregistering failed")
452+
}
453+
err = reg.Register(wrapped)
454+
if err != nil {
455+
t.Fatal("registering failed:", err)
456+
}
457+
458+
gathered, err := reg.Gather()
459+
if err != nil {
460+
t.Fatal("gathering failed:", err)
461+
}
462+
463+
lg := NewGauge(GaugeOpts{Name: "testing", ConstLabels: Labels{"lbl": "1"}})
464+
lg.Set(42)
465+
expected := toMetricFamilies(lg)
466+
assertEqualMFs(t, expected, gathered)
467+
})
468+
}

0 commit comments

Comments
 (0)