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

name composer: fast and slow (reflect-based) implementations #79

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
153 changes: 153 additions & 0 deletions composer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package metrics

import (
"fmt"
"reflect"
"strings"
)

// LabelComposer lets you compose valid labels string
// Implement this interface if your want fast labels generation
// Otherwise it will fall back to a slow reflection-based implementation
type LabelComposer interface {
ToLabelsString() string
}

// NameCompose returns a valid full metric name, composed of a metric name + stringified labels
// It accepts a valid LabelComposer interface, which is used to compose labels string.
//
// The NameCompose can be called for further GetOrCreateCounter/etc func:
//
// // `my_counter{status="active",flag="false"}`
// GetOrCreateCounter(NameCompose("my_counter", MyLabels{
// Status: "active",
// Flag: false,
// })).Inc()
func NameCompose(name string, lc LabelComposer) string {
if lc == nil {
return name
}

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
}

return name + reflectLabelCompose(lc)
}

// reflectLabelCompose composes labels string {field="value",...} from a struct
// 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 labelComposerAutoMarker) string {
labelsStr := "{"

val := reflect.Indirect(reflect.ValueOf(lc))
typ := val.Type()

var n int
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Anonymous || !field.IsExported() {
continue
}
ft := field.Type
if field.Type.Kind() == reflect.Pointer {
ft = field.Type.Elem()
}
fk := ft.Kind()

// We only support basic scalar types: Strings, Numbers, Bool
if fk != reflect.String && fk != reflect.Bool && (fk < reflect.Int || fk > reflect.Uint64) {
continue
}

var labelName string
if ourTag := field.Tag.Get(labelsTag); ourTag != "" {
if ourTag == "-" { // tag="-" means "skip this field"
continue
}
labelName = ourTag
} else {
labelName = toSnakeCase(field.Name)
}

if n > 0 {
labelsStr += ","
}
labelsStr += labelName + `="` + stringifyLabelValue(val.Field(i)) + `"`
n++
}

return labelsStr + "}"
}

// labelsTag is the tag name used for labels inside structs.
// The tag is optional, as if not present, field is used with snake_cased FieldName.
// It's useful to use a tag when you want to override the default naming or exclude a field from the metric.
var labelsTag = "labels"

// SetLabelsStructTag sets the tag name used for labels inside structs.
func SetLabelsStructTag(tag string) {
labelsTag = tag
}

// stringifyLabelValue makes up a valid string value from a given field's value
// It's used ONLY in fallback reflect mode
// Field value might be a pointer, that's why we do reflect.Indirect()
// Note: in future we can handle default values here as well
func stringifyLabelValue(v reflect.Value) string {
k := v.Kind()
if k == reflect.Ptr {
if v.IsNil() {
return "nil"
}
v = v.Elem()
}

return fmt.Sprintf("%v", v.Interface())
}

// Convert struct field names to snake_case for Prometheus label compliance.
func toSnakeCase(s string) string {
s = strings.TrimSpace(s)
var result []rune
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result = append(result, '_')
}
result = append(result, r)
}
return strings.ToLower(string(result))
}
56 changes: 56 additions & 0 deletions composer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package metrics

import (
"fmt"
"testing"
)

// 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 {
AutoLabelComposer
Status string
Flag bool
}

func (m *MyLabelsFast) ToLabelsString() string {
return "{" +
`status="` + m.Status + `",` +
`flag="` + fmt.Sprintf("%t", m.Flag) + `"` +
"}"
}

func TestLabelComposeWithoutReflect(t *testing.T) {
want := `my_counter{status="active",flag="true"}`
got := NameCompose("my_counter", &MyLabelsFast{
Status: "active", Flag: true,
})

if want != got {
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)
}
}