Skip to content

Commit c638139

Browse files
committed
fix duplicate custom metrics when multiple tags are used (#5491)
1 parent 55255b0 commit c638139

File tree

4 files changed

+169
-103
lines changed

4 files changed

+169
-103
lines changed

packages/dd-trace/src/dogstatsd.js

+94-77
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ class DogStatsDClient {
194194
}
195195
}
196196

197-
// TODO: Handle arrays of tags and tags translation.
198197
class MetricsAggregationClient {
199198
constructor (client) {
200199
this._client = client
@@ -211,98 +210,132 @@ class MetricsAggregationClient {
211210
}
212211

213212
reset () {
214-
this._counters = {}
215-
this._gauges = {}
216-
this._histograms = {}
213+
this._counters = new Map()
214+
this._gauges = new Map()
215+
this._histograms = new Map()
217216
}
218217

219-
distribution (name, value, tag) {
220-
this._client.distribution(name, value, tag && [tag])
218+
// TODO: Aggerate with a histogram and send the buckets to the client.
219+
distribution (name, value, tags) {
220+
this._client.distribution(name, value, tags)
221221
}
222222

223-
boolean (name, value, tag) {
224-
this.gauge(name, value ? 1 : 0, tag)
223+
boolean (name, value, tags) {
224+
this.gauge(name, value ? 1 : 0, tags)
225225
}
226226

227-
histogram (name, value, tag) {
228-
this._histograms[name] = this._histograms[name] || new Map()
227+
histogram (name, value, tags) {
228+
const node = this._ensureTree(this._histograms, name, tags, null)
229229

230-
if (!this._histograms[name].has(tag)) {
231-
this._histograms[name].set(tag, new Histogram())
230+
if (!node.value) {
231+
node.value = new Histogram()
232232
}
233233

234-
this._histograms[name].get(tag).record(value)
234+
node.value.record(value)
235235
}
236236

237-
count (name, count, tag, monotonic = true) {
238-
if (typeof tag === 'boolean') {
239-
monotonic = tag
240-
tag = undefined
237+
count (name, count, tags = [], monotonic = true) {
238+
if (typeof tags === 'boolean') {
239+
monotonic = tags
240+
tags = []
241241
}
242242

243-
const map = monotonic ? this._counters : this._gauges
243+
const container = monotonic ? this._counters : this._gauges
244+
const node = this._ensureTree(container, name, tags, 0)
244245

245-
map[name] = map[name] || new Map()
246-
247-
const value = map[name].get(tag) || 0
248-
249-
map[name].set(tag, value + count)
246+
node.value = node.value + count
250247
}
251248

252-
gauge (name, value, tag) {
253-
this._gauges[name] = this._gauges[name] || new Map()
254-
this._gauges[name].set(tag, value)
249+
gauge (name, value, tags) {
250+
const node = this._ensureTree(this._gauges, name, tags, 0)
251+
252+
node.value = value
255253
}
256254

257-
increment (name, count = 1, tag) {
258-
this.count(name, count, tag)
255+
increment (name, count = 1, tags) {
256+
this.count(name, count, tags)
259257
}
260258

261-
decrement (name, count = 1, tag) {
262-
this.count(name, -count, tag)
259+
decrement (name, count = 1, tags) {
260+
this.count(name, -count, tags)
263261
}
264262

265263
_captureGauges () {
266-
Object.keys(this._gauges).forEach(name => {
267-
this._gauges[name].forEach((value, tag) => {
268-
this._client.gauge(name, value, tag && [tag])
269-
})
264+
this._captureTree(this._gauges, (node, name, tags) => {
265+
this._client.gauge(name, node.value, tags)
270266
})
271267
}
272268

273269
_captureCounters () {
274-
Object.keys(this._counters).forEach(name => {
275-
this._counters[name].forEach((value, tag) => {
276-
this._client.increment(name, value, tag && [tag])
277-
})
270+
this._captureTree(this._counters, (node, name, tags) => {
271+
this._client.increment(name, node.value, tags)
278272
})
279273

280-
this._counters = {}
274+
this._counters.clear()
281275
}
282276

283277
_captureHistograms () {
284-
Object.keys(this._histograms).forEach(name => {
285-
this._histograms[name].forEach((stats, tag) => {
286-
const tags = tag && [tag]
278+
this._captureTree(this._histograms, (node, name, tags) => {
279+
let stats = node.value
287280

288-
// Stats can contain garbage data when a value was never recorded.
289-
if (stats.count === 0) {
290-
stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0, reset: stats.reset }
291-
}
281+
// Stats can contain garbage data when a value was never recorded.
282+
if (stats.count === 0) {
283+
stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0 }
284+
}
292285

293-
this._client.gauge(`${name}.min`, stats.min, tags)
294-
this._client.gauge(`${name}.max`, stats.max, tags)
295-
this._client.increment(`${name}.sum`, stats.sum, tags)
296-
this._client.increment(`${name}.total`, stats.sum, tags)
297-
this._client.gauge(`${name}.avg`, stats.avg, tags)
298-
this._client.increment(`${name}.count`, stats.count, tags)
299-
this._client.gauge(`${name}.median`, stats.median, tags)
300-
this._client.gauge(`${name}.95percentile`, stats.p95, tags)
286+
this._client.gauge(`${name}.min`, stats.min, tags)
287+
this._client.gauge(`${name}.max`, stats.max, tags)
288+
this._client.increment(`${name}.sum`, stats.sum, tags)
289+
this._client.increment(`${name}.total`, stats.sum, tags)
290+
this._client.gauge(`${name}.avg`, stats.avg, tags)
291+
this._client.increment(`${name}.count`, stats.count, tags)
292+
this._client.gauge(`${name}.median`, stats.median, tags)
293+
this._client.gauge(`${name}.95percentile`, stats.p95, tags)
301294

302-
stats.reset()
303-
})
295+
node.value.reset()
304296
})
305297
}
298+
299+
_captureTree (tree, fn) {
300+
for (const [name, root] of tree) {
301+
this._captureNode(root, name, [], fn)
302+
}
303+
}
304+
305+
_captureNode (node, name, tags, fn) {
306+
if (node.touched) {
307+
fn(node, name, tags)
308+
}
309+
310+
for (const [tag, next] of node.nodes) {
311+
this._captureNode(next, name, tags.concat(tag), fn)
312+
}
313+
}
314+
315+
_ensureTree (tree, name, tags, value) {
316+
tags = tags ? [].concat(tags) : []
317+
318+
let node = this._ensureNode(tree, name, value)
319+
320+
for (const tag of tags) {
321+
node = this._ensureNode(node.nodes, tag, value)
322+
}
323+
324+
node.touched = true
325+
326+
return node
327+
}
328+
329+
_ensureNode (container, key, value) {
330+
let node = container.get(key)
331+
332+
if (!node) {
333+
node = { nodes: new Map(), touched: false, value }
334+
container.set(key, node)
335+
}
336+
337+
return node
338+
}
306339
}
307340

308341
/**
@@ -324,45 +357,29 @@ class CustomMetrics {
324357
}
325358

326359
increment (stat, value = 1, tags) {
327-
for (const tag of this._normalizeTags(tags)) {
328-
this._client.increment(stat, value, tag)
329-
}
360+
this._client.increment(stat, value, CustomMetrics.tagTranslator(tags))
330361
}
331362

332363
decrement (stat, value = 1, tags) {
333-
for (const tag of this._normalizeTags(tags)) {
334-
this._client.decrement(stat, value, tag)
335-
}
364+
this._client.decrement(stat, value, CustomMetrics.tagTranslator(tags))
336365
}
337366

338367
gauge (stat, value, tags) {
339-
for (const tag of this._normalizeTags(tags)) {
340-
this._client.gauge(stat, value, tag)
341-
}
368+
this._client.gauge(stat, value, CustomMetrics.tagTranslator(tags))
342369
}
343370

344371
distribution (stat, value, tags) {
345-
for (const tag of this._normalizeTags(tags)) {
346-
this._client.distribution(stat, value, tag)
347-
}
372+
this._client.distribution(stat, value, CustomMetrics.tagTranslator(tags))
348373
}
349374

350375
histogram (stat, value, tags) {
351-
for (const tag of this._normalizeTags(tags)) {
352-
this._client.histogram(stat, value, tag)
353-
}
376+
this._client.histogram(stat, value, CustomMetrics.tagTranslator(tags))
354377
}
355378

356379
flush () {
357380
return this._client.flush()
358381
}
359382

360-
_normalizeTags (tags) {
361-
tags = CustomMetrics.tagTranslator(tags)
362-
363-
return tags.length === 0 ? [undefined] : tags
364-
}
365-
366383
/**
367384
* Exposing { tagName: 'tagValue' } to the end user
368385
* These are translated into [ 'tagName:tagValue' ] for internal use

packages/dd-trace/src/histogram.js

+12-23
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,28 @@ class Histogram {
77
this.reset()
88
}
99

10-
get min () { return this._min }
11-
get max () { return this._max }
12-
get avg () { return this._count === 0 ? 0 : this._sum / this._count }
13-
get sum () { return this._sum }
14-
get count () { return this._count }
10+
get min () { return this._sketch.count === 0 ? 0 : this._sketch.min }
11+
get max () { return this._sketch.count === 0 ? 0 : this._sketch.max }
12+
get avg () { return this._sketch.count === 0 ? 0 : this._sketch.sum / this._sketch.count }
13+
get sum () { return this._sketch.sum }
14+
get count () { return this._sketch.count }
1515
get median () { return this.percentile(50) }
1616
get p95 () { return this.percentile(95) }
1717

1818
percentile (percentile) {
19-
return this._histogram.getValueAtQuantile(percentile / 100) || 0
19+
return this._sketch.getValueAtQuantile(percentile / 100) || 0
2020
}
2121

22-
record (value) {
23-
if (this._count === 0) {
24-
this._min = this._max = value
25-
} else {
26-
this._min = Math.min(this._min, value)
27-
this._max = Math.max(this._max, value)
28-
}
29-
30-
this._count++
31-
this._sum += value
22+
merge (histogram) {
23+
return this._sketch.merge(histogram._sketch)
24+
}
3225

33-
this._histogram.accept(value)
26+
record (value) {
27+
this._sketch.accept(value)
3428
}
3529

3630
reset () {
37-
this._min = 0
38-
this._max = 0
39-
this._sum = 0
40-
this._count = 0
41-
42-
this._histogram = new DDSketch()
31+
this._sketch = new DDSketch()
4332
}
4433
}
4534

packages/dd-trace/test/dogstatsd.spec.js

+59
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,21 @@ describe('dogstatsd', () => {
374374
expect(udp4.send.firstCall.args[0].toString()).to.equal('test.avg:10|g|#foo:bar\n')
375375
})
376376

377+
it('.gauge() with tags', () => {
378+
client = new CustomMetrics({ dogstatsd: {} })
379+
380+
client.gauge('test.avg', 10, { foo: 'bar' })
381+
client.gauge('test.avg', 10, { foo: 'bar', baz: 'qux' })
382+
client.gauge('test.avg', 20, { foo: 'bar', baz: 'qux' })
383+
client.flush()
384+
385+
expect(udp4.send).to.have.been.called
386+
expect(udp4.send.firstCall.args[0].toString()).to.equal([
387+
'test.avg:10|g|#foo:bar',
388+
'test.avg:20|g|#foo:bar,baz:qux'
389+
].join('\n') + '\n')
390+
})
391+
377392
it('.increment()', () => {
378393
client = new CustomMetrics({ dogstatsd: {} })
379394

@@ -396,6 +411,21 @@ describe('dogstatsd', () => {
396411
expect(udp4.send.firstCall.args[0].toString()).to.equal('test.count:2|c\n')
397412
})
398413

414+
it('.increment() with tags', () => {
415+
client = new CustomMetrics({ dogstatsd: {} })
416+
417+
client.increment('test.count', 10, { foo: 'bar' })
418+
client.increment('test.count', 10, { foo: 'bar', baz: 'qux' })
419+
client.increment('test.count', 10, { foo: 'bar', baz: 'qux' })
420+
client.flush()
421+
422+
expect(udp4.send).to.have.been.called
423+
expect(udp4.send.firstCall.args[0].toString()).to.equal([
424+
'test.count:10|c|#foo:bar',
425+
'test.count:20|c|#foo:bar,baz:qux'
426+
].join('\n') + '\n')
427+
})
428+
399429
it('.decrement()', () => {
400430
client = new CustomMetrics({ dogstatsd: {} })
401431

@@ -449,6 +479,35 @@ describe('dogstatsd', () => {
449479
].join('\n') + '\n')
450480
})
451481

482+
it('.histogram() with tags', () => {
483+
client = new CustomMetrics({ dogstatsd: {} })
484+
485+
client.histogram('test.histogram', 10, { foo: 'bar' })
486+
client.histogram('test.histogram', 10, { foo: 'bar', baz: 'qux' })
487+
client.histogram('test.histogram', 10, { foo: 'bar', baz: 'qux' })
488+
client.flush()
489+
490+
expect(udp4.send).to.have.been.called
491+
expect(udp4.send.firstCall.args[0].toString()).to.equal([
492+
'test.histogram.min:10|g|#foo:bar',
493+
'test.histogram.max:10|g|#foo:bar',
494+
'test.histogram.sum:10|c|#foo:bar',
495+
'test.histogram.total:10|c|#foo:bar',
496+
'test.histogram.avg:10|g|#foo:bar',
497+
'test.histogram.count:1|c|#foo:bar',
498+
'test.histogram.median:10.074696689511441|g|#foo:bar',
499+
'test.histogram.95percentile:10.074696689511441|g|#foo:bar',
500+
'test.histogram.min:10|g|#foo:bar,baz:qux',
501+
'test.histogram.max:10|g|#foo:bar,baz:qux',
502+
'test.histogram.sum:20|c|#foo:bar,baz:qux',
503+
'test.histogram.total:20|c|#foo:bar,baz:qux',
504+
'test.histogram.avg:10|g|#foo:bar,baz:qux',
505+
'test.histogram.count:2|c|#foo:bar,baz:qux',
506+
'test.histogram.median:10.074696689511441|g|#foo:bar,baz:qux',
507+
'test.histogram.95percentile:10.074696689511441|g|#foo:bar,baz:qux'
508+
].join('\n') + '\n')
509+
})
510+
452511
it('should flush via interval', () => {
453512
const clock = sinon.useFakeTimers()
454513

0 commit comments

Comments
 (0)