diff --git a/engine.go b/engine.go index 15f049c..7f19d83 100644 --- a/engine.go +++ b/engine.go @@ -29,6 +29,12 @@ type Engine struct { // that manipulates this field directly has to respect this requirement. Tags []Tag + // Indicates whether to allow duplicated tags from the tags list before sending. + // This option is turned off by default, ensuring that duplicate tags are removed. + // Turn it on if you need to send the same tag multiple times with different values, + // which is a special use case. + AllowDuplicateTags bool + // This cache keeps track of the generated measure structures to avoid // rebuilding them every time a same measure type is seen by the engine. // @@ -148,7 +154,7 @@ func (eng *Engine) measure(t time.Time, name string, value interface{}, ftype Fi m.Tags = append(m.Tags[:0], eng.Tags...) m.Tags = append(m.Tags, tags...) - if len(tags) != 0 && !TagsAreSorted(m.Tags) { + if len(tags) != 0 && !eng.AllowDuplicateTags && !TagsAreSorted(m.Tags) { SortTags(m.Tags) } @@ -192,7 +198,9 @@ func (eng *Engine) ReportAt(time time.Time, metrics interface{}, tags ...Tag) { tb = tagsPool.Get().(*tagsBuffer) tb.append(tags...) tb.append(eng.Tags...) - tb.sort() + if !eng.AllowDuplicateTags { + tb.sort() + } tags = tb.tags } diff --git a/engine_test.go b/engine_test.go index 31aaa09..5d0c678 100644 --- a/engine_test.go +++ b/engine_test.go @@ -68,6 +68,10 @@ func TestEngine(t *testing.T) { scenario: "calling Engine.WithTags produces expected tags", function: testEngineWithTags, }, + { + scenario: "calling Engine.Incr produces expected tags when AllowDuplicateTags is set", + function: testEngineAllowDuplicateTags, + }, } for _, test := range tests { @@ -126,6 +130,29 @@ func testEngineFlush(t *testing.T, eng *stats.Engine) { } } +func testEngineAllowDuplicateTags(t *testing.T, eng *stats.Engine) { + e2 := eng.WithTags() + e2.AllowDuplicateTags = true + if e2.Prefix != "test" { + t.Error("bad prefix:", e2.Prefix) + } + e2.Incr("measure.count") + e2.Incr("measure.count", stats.T("category", "a"), stats.T("category", "b"), stats.T("category", "c")) + + checkMeasuresEqual(t, e2, + stats.Measure{ + Name: "test.measure", + Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, + Tags: []stats.Tag{stats.T("service", "test-service")}, + }, + stats.Measure{ + Name: "test.measure", + Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, + Tags: []stats.Tag{stats.T("service", "test-service"), stats.T("category", "a"), stats.T("category", "b"), stats.T("category", "c")}, + }, + ) +} + func testEngineIncr(t *testing.T, eng *stats.Engine) { eng.Incr("measure.count") eng.Incr("measure.count", stats.T("type", "testing")) diff --git a/tag.go b/tag.go index 1edbf0c..9e421ce 100644 --- a/tag.go +++ b/tag.go @@ -38,10 +38,9 @@ func TagsAreSorted(tags []Tag) bool { return slices.IsSortedFunc(tags, tagCompare) } -// SortTags sorts and deduplicates tags in-place, -// favoring later elements whenever a tag name duplicate occurs. -// The returned slice may be shorter than the input -// due to the elimination of duplicates. +// SortTags sorts and deduplicates tags in-place, favoring later elements +// whenever a tag name duplicate occurs. The returned slice may be shorter than +// the input due to the elimination of duplicates. func SortTags(tags []Tag) []Tag { // Stable sort ensures that we have deterministic // "latest wins" deduplication.