diff --git a/composer.go b/composer.go index 60a4f6c..6d60bd0 100644 --- a/composer.go +++ b/composer.go @@ -13,22 +13,8 @@ type LabelComposer interface { ToLabelsString() string } -// labelComposerMarker is a marker interface for enforcing type-safety of StructLabelComposer. -// Interface is private so it's only be used via StructLabelComposer implementation. -// This is made for safety reasons, so it's not allowed to pass a random struct to NameCompose() function. -type labelComposerMarker interface { - labelComposerMarker() -} - -// StructLabelComposer MUST be embedded in any struct that serves as a label composer. -// Embedding is required even if you provide custom implementation of LabelComposer (ToLabelsString() method) -type StructLabelComposer struct{} - -func (s StructLabelComposer) labelComposerMarker() { panic("should never happen") } - // NameCompose returns a valid full metric name, composed of a metric name + stringified labels -// It will first try to use custom LabelComposer implementation (if any) -// then fallback to slow reflection-based implementation. +// It accepts a valid LabelComposer interface, which is used to compose labels string. // // The NameCompose can be called for further GetOrCreateCounter/etc func: // @@ -37,19 +23,46 @@ func (s StructLabelComposer) labelComposerMarker() { panic("should never happen" // Status: "active", // Flag: false, // })).Inc() -func NameCompose(name string, lc labelComposerMarker) string { +func NameCompose(name string, lc LabelComposer) string { if lc == nil { return name } - // Implementing public LabelComposer means we must implement - // a custom ToLabelsString() that supposed to be fast. - if v, ok := lc.(LabelComposer); ok { - return name + v.ToLabelsString() + return name + lc.ToLabelsString() +} + +// +// Auto composer +// + +// labelComposerAutoMarker is just a marker interface. +// Interface is private so it's only be used via AutoLabelComposer implementation. +// This is made for safety reasons, so it's not allowed to pass a random struct to NameCompose() function. +type labelComposerAutoMarker interface { + autoComposeMarker() +} + +// AutoLabelComposer MUST be embedded in any struct that serves as a label composer. +// Embedding is required even if you provide custom implementation of LabelComposer (ToLabelsString() method) +type AutoLabelComposer struct{} + +func (s AutoLabelComposer) autoComposeMarker() { panic("should never happen") } + +// NameComposeAuto returns a valid full metric name, composed of a metric name + stringified labels +// It accepts a struct who embeds AutoLabelComposer so labels are generated from it. +// +// The NameComposeAuto can be called for further GetOrCreateCounter/etc func: +// +// // `my_counter{status="active",flag="false"}` +// GetOrCreateCounter(NameComposeAuto("my_counter", MyLabels{ +// Status: "active", +// Flag: false, +// })).Inc() +func NameComposeAuto(name string, lc labelComposerAutoMarker) string { + if lc == nil { + return name } - // falling back to reflect-based implementation - // This is considered to be slow. Implement your own LabelComposer if it's an issue for you. return name + reflectLabelCompose(lc) } @@ -57,7 +70,7 @@ func NameCompose(name string, lc labelComposerMarker) string { // It will use only exported scalar fields, and will skip fields with the `-` tag. // By default, the snake_cased field name is used as the label name. // Label's name can be overridden by using the `labels` tag -func reflectLabelCompose(lc labelComposerMarker) string { +func reflectLabelCompose(lc labelComposerAutoMarker) string { labelsStr := "{" val := reflect.Indirect(reflect.ValueOf(lc)) diff --git a/composer_test.go b/composer_test.go index 70c7bcb..d5e9d49 100644 --- a/composer_test.go +++ b/composer_test.go @@ -5,34 +5,11 @@ import ( "testing" ) -// MyLabelsSlow will be converted into {hello="world",enabled="true"} -// via reflect implementation. -// It's slow but completely automatic. You don't need to write any code -type MyLabelsSlow struct { - StructLabelComposer - - Status string - Flag bool -} - -func TestLabelComposeWithReflect(t *testing.T) { - want := `my_counter{status="active",flag="true"}` - - got := NameCompose("my_counter", MyLabelsSlow{ - Status: "active", - Flag: true, - }) - - if got != want { - t.Fatalf("unexpected full name; got %q; want %q", got, want) - } -} - // MyLabelsFast will be converted into string // via custom implementation (Using ToLabelsString() method of LabelComposer interface) // It's fast but requires manual implementation. type MyLabelsFast struct { - StructLabelComposer + AutoLabelComposer Status string Flag bool } @@ -40,7 +17,7 @@ type MyLabelsFast struct { func (m *MyLabelsFast) ToLabelsString() string { return "{" + `status="` + m.Status + `",` + - `flag="` + fmt.Sprintf("%v", m.Flag) + `"` + + `flag="` + fmt.Sprintf("%t", m.Flag) + `"` + "}" } @@ -54,3 +31,26 @@ func TestLabelComposeWithoutReflect(t *testing.T) { t.Fatalf("unexpected full name; got %q; want %q", got, want) } } + +func TestLabelComposeWithReflect(t *testing.T) { + want := `my_counter{status="active",flag="true"}` + + // MyLabelsSlow will be converted into {hello="world",enabled="true"} + // via reflect implementation. + // It's slow but completely automatic. You don't need to write any code + type MyLabelsAuto struct { + AutoLabelComposer + + Status string + Flag bool + } + + got := NameComposeAuto("my_counter", MyLabelsAuto{ + Status: "active", + Flag: true, + }) + + if got != want { + t.Fatalf("unexpected full name; got %q; want %q", got, want) + } +}