diff --git a/stats.go b/stats.go index e187e0f..2130faa 100644 --- a/stats.go +++ b/stats.go @@ -62,6 +62,10 @@ type Scope interface { // Scope creates a subscope. Scope(name string) Scope + // ScopeWithTags creates a subscope with Tags to a store or scope. All child scopes and metrics + // will inherit these tags by default. + ScopeWithTags(name string, tags map[string]string) Scope + // Store returns the Scope's backing Store. Store() Store @@ -203,6 +207,7 @@ func NewDefaultStore() Store { type subScope struct { registry *statStore name string + tags map[string]string } type counter struct { @@ -371,10 +376,21 @@ func (s *statStore) Store() Store { } func (s *statStore) Scope(name string) Scope { - return subScope{registry: s, name: name} + return s.ScopeWithTags(name, nil) +} + +func (s *statStore) ScopeWithTags(name string, tags map[string]string) Scope { + return subScope{registry: s, name: name, tags: tags} } func (s *statStore) NewCounter(name string) Counter { + return s.NewCounterWithTags(name, nil) +} + +func (s *statStore) NewCounterWithTags(name string, tags map[string]string) Counter { + serializedTags := serializeTags(tags) + name = fmt.Sprintf("%s%s", name, serializedTags) + s.countersMtx.RLock() c, ok := s.counters[name] s.countersMtx.RUnlock() @@ -398,12 +414,6 @@ func (s *statStore) NewCounter(name string) Counter { s.countersMtx.Unlock() return c - -} - -func (s *statStore) NewCounterWithTags(name string, tags map[string]string) Counter { - serializedTags := serializeTags(tags) - return s.NewCounter(fmt.Sprintf("%s%s", name, serializedTags)) } func (s *statStore) NewPerInstanceCounter(name string, tags map[string]string) Counter { @@ -414,11 +424,18 @@ func (s *statStore) NewPerInstanceCounter(name string, tags map[string]string) C if _, found := tags["_f"]; !found { tags["_f"] = "i" } - serializedTags := serializeTags(tags) - return s.NewCounter(fmt.Sprintf("%s%s", name, serializedTags)) + + return s.NewCounterWithTags(name, tags) } func (s *statStore) NewGauge(name string) Gauge { + return s.NewGaugeWithTags(name, nil) +} + +func (s *statStore) NewGaugeWithTags(name string, tags map[string]string) Gauge { + serializedTags := serializeTags(tags) + name = fmt.Sprintf("%s%s", name, serializedTags) + s.gaugesMtx.RLock() g, ok := s.gauges[name] s.gaugesMtx.RUnlock() @@ -438,12 +455,6 @@ func (s *statStore) NewGauge(name string) Gauge { } return g - -} - -func (s *statStore) NewGaugeWithTags(name string, tags map[string]string) Gauge { - serializedTags := serializeTags(tags) - return s.NewGauge(fmt.Sprintf("%s%s", name, serializedTags)) } func (s *statStore) NewPerInstanceGauge(name string, tags map[string]string) Gauge { @@ -454,11 +465,18 @@ func (s *statStore) NewPerInstanceGauge(name string, tags map[string]string) Gau if _, found := tags["_f"]; !found { tags["_f"] = "i" } - serializedTags := serializeTags(tags) - return s.NewGauge(fmt.Sprintf("%s%s", name, serializedTags)) + + return s.NewGaugeWithTags(name, tags) } func (s *statStore) NewTimer(name string) Timer { + return s.NewTimerWithTags(name, nil) +} + +func (s *statStore) NewTimerWithTags(name string, tags map[string]string) Timer { + serializedTags := serializeTags(tags) + name = fmt.Sprintf("%s%s", name, serializedTags) + s.timersMtx.RLock() t, ok := s.timers[name] s.timersMtx.RUnlock() @@ -474,12 +492,6 @@ func (s *statStore) NewTimer(name string) Timer { s.timersMtx.Unlock() return t - -} - -func (s *statStore) NewTimerWithTags(name string, tags map[string]string) Timer { - serializedTags := serializeTags(tags) - return s.NewTimer(fmt.Sprintf("%s%s", name, serializedTags)) } func (s *statStore) NewPerInstanceTimer(name string, tags map[string]string) Timer { @@ -490,50 +502,71 @@ func (s *statStore) NewPerInstanceTimer(name string, tags map[string]string) Tim if _, found := tags["_f"]; !found { tags["_f"] = "i" } - serializedTags := serializeTags(tags) - return s.NewTimer(fmt.Sprintf("%s%s", name, serializedTags)) + + return s.NewTimerWithTags(name, tags) } func (s subScope) Scope(name string) Scope { return &subScope{registry: s.registry, name: fmt.Sprintf("%s.%s", s.name, name)} } +func (s subScope) ScopeWithTags(name string, tags map[string]string) Scope { + return &subScope{registry: s.registry, name: fmt.Sprintf("%s.%s", s.name, name), tags: s.mergeTags(tags)} +} + func (s subScope) Store() Store { return s.registry } func (s subScope) NewCounter(name string) Counter { - return s.registry.NewCounter(fmt.Sprintf("%s.%s", s.name, name)) + return s.NewCounterWithTags(name, nil) } func (s subScope) NewCounterWithTags(name string, tags map[string]string) Counter { - return s.registry.NewCounterWithTags(fmt.Sprintf("%s.%s", s.name, name), tags) + return s.registry.NewCounterWithTags(fmt.Sprintf("%s.%s", s.name, name), s.mergeTags(tags)) } func (s subScope) NewPerInstanceCounter(name string, tags map[string]string) Counter { - return s.registry.NewPerInstanceCounter(fmt.Sprintf("%s.%s", s.name, name), tags) + return s.registry.NewPerInstanceCounter(fmt.Sprintf("%s.%s", s.name, name), s.mergeTags(tags)) } func (s subScope) NewGauge(name string) Gauge { - return s.registry.NewGauge(fmt.Sprintf("%s.%s", s.name, name)) + return s.NewGaugeWithTags(name, nil) } func (s subScope) NewGaugeWithTags(name string, tags map[string]string) Gauge { - return s.registry.NewGaugeWithTags(fmt.Sprintf("%s.%s", s.name, name), tags) + return s.registry.NewGaugeWithTags(fmt.Sprintf("%s.%s", s.name, name), s.mergeTags(tags)) } func (s subScope) NewPerInstanceGauge(name string, tags map[string]string) Gauge { - return s.registry.NewPerInstanceGauge(fmt.Sprintf("%s.%s", s.name, name), tags) + return s.registry.NewPerInstanceGauge(fmt.Sprintf("%s.%s", s.name, name), s.mergeTags(tags)) } func (s subScope) NewTimer(name string) Timer { - return s.registry.NewTimer(fmt.Sprintf("%s.%s", s.name, name)) + return s.NewTimerWithTags(name, nil) } func (s subScope) NewTimerWithTags(name string, tags map[string]string) Timer { - return s.registry.NewTimerWithTags(fmt.Sprintf("%s.%s", s.name, name), tags) + return s.registry.NewTimerWithTags(fmt.Sprintf("%s.%s", s.name, name), s.mergeTags(tags)) } func (s subScope) NewPerInstanceTimer(name string, tags map[string]string) Timer { - return s.registry.NewPerInstanceTimer(fmt.Sprintf("%s.%s", s.name, name), tags) + return s.registry.NewPerInstanceTimer(fmt.Sprintf("%s.%s", s.name, name), s.mergeTags(tags)) +} + +// mergeTags augments tags with all scope-level tags that are not already present. +// Modifies and returns tags directly. +func (s subScope) mergeTags(tags map[string]string) map[string]string { + if len(s.tags) == 0 { + return tags + } + if tags == nil { + tags = make(map[string]string) + } + for k, v := range s.tags { + if _, ok := tags[k]; !ok { + tags[k] = v + } + } + return tags } diff --git a/tags.go b/tags.go index 1870618..972ead2 100644 --- a/tags.go +++ b/tags.go @@ -28,6 +28,9 @@ func (t tagSet) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t tagSet) Less(i, j int) bool { return t[i].dimension < t[j].dimension } func serializeTags(tags map[string]string) string { + if len(tags) == 0 { + return "" + } tagPairs := make([]tagPair, 0, len(tags)) for tagKey, tagValue := range tags { tagValue = illegalTagValueChars.ReplaceAllLiteralString(tagValue, tagFailsafe) diff --git a/tcp_sink_test.go b/tcp_sink_test.go index 86a8c40..ab2ab71 100644 --- a/tcp_sink_test.go +++ b/tcp_sink_test.go @@ -287,6 +287,46 @@ func TestScopes(t *testing.T) { } } +func TestScopesWithTags(t *testing.T) { + sink := &testStatSink{} + store := NewStore(sink, true) + + ascope := store.ScopeWithTags("a", map[string]string{"x": "a", "y": "a"}) + bscope := ascope.ScopeWithTags("b", map[string]string{"x": "b", "z": "b"}) + counter := bscope.NewCounter("c") + counter.Inc() + timer := bscope.NewTimer("t") + timer.AddValue(1) + gauge := bscope.NewGauge("g") + gauge.Set(1) + store.Flush() + + expected := "a.b.t.__x=b.__y=a.__z=b:1.000000|ms\na.b.c.__x=b.__y=a.__z=b:1|c\na.b.g.__x=b.__y=a.__z=b:1|g\n" + if expected != sink.record { + t.Errorf("Expected: '%s' Got: '%s'", expected, sink.record) + } +} + +func TestScopesAndMetricsWithTags(t *testing.T) { + sink := &testStatSink{} + store := NewStore(sink, true) + + ascope := store.ScopeWithTags("a", map[string]string{"x": "a", "y": "a"}) + bscope := ascope.Scope("b") + counter := bscope.NewCounterWithTags("c", map[string]string{"x": "m", "z": "m"}) + counter.Inc() + timer := bscope.NewTimerWithTags("t", map[string]string{"x": "m", "z": "m"}) + timer.AddValue(1) + gauge := bscope.NewGaugeWithTags("g", map[string]string{"x": "m", "z": "m"}) + gauge.Set(1) + store.Flush() + + expected := "a.b.t.__x=m.__z=m:1.000000|ms\na.b.c.__x=m.__z=m:1|c\na.b.g.__x=m.__z=m:1|g\n" + if expected != sink.record { + t.Errorf("Expected: '%s' Got: '%s'", expected, sink.record) + } +} + type testStatGenerator struct { counter Counter gauge Gauge