Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

## Unreleased

* @bdeitte Fill in some missing areas for automated tests

## 14.0.0 (2026-2-15)

* @bdeitte Fix methods losing parameters when given empty object for sampleRate. Fixes #43
Expand Down
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ npx mocha test/specific-test.js --timeout 5000
- Can override prefix, suffix, globalTags
- Nested child clients supported

### Timer Context (Dynamic Tags)
`timer`, `asyncTimer`, and `asyncDistTimer` pass a `ctx` object as the last argument to
wrapped functions, enabling dynamic tags to be added during execution:

```javascript
const wrappedFn = statsd.timer(function (arg1, ctx) {
ctx.addTags({ result: 'success' }); // tags added at metric send time
// ... fn body
}, 'my.metric');
```

The `ctx` object has `addTags(tags)` to attach tags that are merged with any
static tags when the timing metric is sent.

## Protocol-Specific Features

### DataDog (DogStatsD)
Expand Down
102 changes: 102 additions & 0 deletions test/buffer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const assert = require('assert');
const helpers = require('./helpers/helpers.js');
const sinon = require('sinon');

const closeAll = helpers.closeAll;
const testTypes = helpers.testTypes;
Expand All @@ -9,8 +10,13 @@ const createHotShotsClient = helpers.createHotShotsClient;
describe('#buffer', () => {
let server;
let statsd;
let clock;

afterEach(done => {
if (clock) {
clock.restore();
clock = null;
}
closeAll(server, statsd, false, done);
server = null;
statsd = null;
Expand Down Expand Up @@ -159,4 +165,100 @@ describe('#buffer', () => {
});
});
});

describe('buffer edge cases', () => {
it('should handle a single metric larger than maxBufferSize', done => {
server = createServer('udp', opts => {
statsd = createHotShotsClient(Object.assign(opts, {
maxBufferSize: 5,
bufferFlushInterval: 50,
}), 'client');
// This metric is larger than maxBufferSize but should still be sent
statsd.increment('a.very.long.metric.name', 1);
});
server.on('metrics', metrics => {
assert.ok(metrics.includes('a.very.long.metric.name:1|c'));
done();
});
});

it('should reset buffer state after flush', done => {
server = createServer('udp', opts => {
statsd = createHotShotsClient(Object.assign(opts, {
maxBufferSize: 500,
bufferFlushInterval: 10000,
}), 'client');

statsd.increment('a', 1);
assert.ok(statsd.bufferHolder.buffer.length > 0);
assert.ok(statsd.bufferLength > 0);

statsd.flushQueue();
assert.strictEqual(statsd.bufferHolder.buffer, '');
assert.strictEqual(statsd.bufferLength, 0);
done();
});
});

it('should share buffer between parent and child clients', done => {
server = createServer('udp', opts => {
const parent = createHotShotsClient(Object.assign(opts, {
maxBufferSize: 500,
}), 'client');
statsd = parent;
const child = parent.childClient({ prefix: 'child.' });

parent.increment('parent.metric', 1);
child.increment('child.metric', 2);

// Both metrics should be in the same buffer
assert.ok(parent.bufferHolder.buffer.includes('parent.metric:1|c'));
assert.ok(parent.bufferHolder.buffer.includes('child.child.metric:2|c'));
assert.strictEqual(parent.bufferHolder, child.bufferHolder);
done();
});
});

it('should handle buffer with emoji and CJK characters correctly', done => {
server = createServer('udp', opts => {
const maxSize = 100;
statsd = createHotShotsClient(Object.assign(opts, {
maxBufferSize: maxSize,
bufferFlushInterval: 10000,
}), 'client');

// Send metrics with multi-byte characters in tags
statsd.increment('metric', 1, { tag: '🎉🎉🎉' });
const bufferSize = Buffer.byteLength(statsd.bufferHolder.buffer);
assert.strictEqual(
bufferSize <= maxSize,
true,
`Buffer size ${bufferSize} exceeded maxBufferSize ${maxSize} with emoji tags`
);
done();
});
});

it('should flush buffer on interval timer', done => {
server = createServer('udp', opts => {
clock = sinon.useFakeTimers();
statsd = createHotShotsClient(Object.assign(opts, {
maxBufferSize: 5000,
bufferFlushInterval: 500,
}), 'client');

statsd.increment('test', 1);
assert.ok(statsd.bufferHolder.buffer.length > 0);

// Advance time past bufferFlushInterval
clock.tick(600);

// Buffer should have been flushed by the interval
assert.strictEqual(statsd.bufferHolder.buffer, '');
assert.strictEqual(statsd.bufferLength, 0);

done();
});
});
});
});
29 changes: 29 additions & 0 deletions test/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,35 @@ describe('#check', () => {
statsd.check('check.name', statsd.CHECKS.OK);
});
});

it('should send all CHECKS status values correctly', done => {
const expectedStatuses = [0, 1, 2, 3]; // OK, WARNING, CRITICAL, UNKNOWN
server = createServer(serverType, opts => {
statsd = createHotShotsClient(opts, clientType);
statsd.check('test.ok', statsd.CHECKS.OK);
statsd.check('test.warning', statsd.CHECKS.WARNING);
statsd.check('test.critical', statsd.CHECKS.CRITICAL);
statsd.check('test.unknown', statsd.CHECKS.UNKNOWN);
});
const onMetrics = event => {
// TCP may concatenate messages, so check all status matches in the event
const statusMatches = event.match(/_sc\|[^|]+\|(\d)/g);
if (statusMatches) {
statusMatches.forEach(match => {
const status = parseInt(match.charAt(match.length - 1), 10);
const index = expectedStatuses.indexOf(status);
if (index >= 0) {
expectedStatuses.splice(index, 1);
}
});
}
if (expectedStatuses.length === 0) {
server.removeListener('metrics', onMetrics);
done();
}
};
server.on('metrics', onMetrics);
});
});
});
});
168 changes: 168 additions & 0 deletions test/childClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,173 @@ describe('#childClient', () => {
done();
});
});

it('should share the parent socket', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(opts, clientType);
statsd = parent;
const child = parent.childClient({ prefix: 'child.' });
assert.strictEqual(child.socket, parent.socket);
done();
});
});

it('should share the parent bufferHolder', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
maxBufferSize: 500,
}), clientType);
statsd = parent;
const child = parent.childClient({ prefix: 'child.' });
assert.strictEqual(child.bufferHolder, parent.bufferHolder);
done();
});
});

it('should support nested child clients (3 levels deep)', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
prefix: 'l1.',
globalTags: ['level:1'],
}), clientType);
statsd = parent;
const child = parent.childClient({
prefix: 'l2.',
globalTags: ['level:2'],
});
const grandchild = child.childClient({
prefix: 'l3.',
globalTags: ['level:3'],
});
grandchild.increment('metric', 1);
});
server.on('metrics', metrics => {
// Prefix should be l3.l2.l1.metric
assert.ok(metrics.includes('l3.l2.l1.metric:1|c'));
// Tag should override: level:3 should win over level:2 and level:1
assert.ok(metrics.includes('level:3'));
assert.ok(!metrics.includes('level:1'));
assert.ok(!metrics.includes('level:2'));
done();
});
});

it('should support nested child clients with suffixes', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
suffix: '.s1',
}), clientType);
statsd = parent;
const child = parent.childClient({ suffix: '.s2' });
const grandchild = child.childClient({ suffix: '.s3' });
grandchild.increment('metric', 1);
});
server.on('metrics', metrics => {
// Suffix should be .s1.s2.s3
assert.ok(metrics.includes('metric.s1.s2.s3:1|c'));
done();
});
});

it('should override duplicate tag keys from parent', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
globalTags: ['env:prod', 'service:api', 'version:1'],
}), clientType);
statsd = parent;
const child = parent.childClient({
globalTags: ['env:staging', 'version:2'],
});
child.increment('metric', 1);
});
server.on('metrics', metrics => {
// env and version should be overridden by child
assert.ok(metrics.includes('env:staging'));
assert.ok(metrics.includes('version:2'));
assert.ok(metrics.includes('service:api'));
assert.ok(!metrics.includes('env:prod'));
assert.ok(!metrics.includes('version:1'));
done();
});
});

it('should support globalTags as object format', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
globalTags: { env: 'prod', service: 'api' },
}), clientType);
statsd = parent;
const child = parent.childClient({
globalTags: { env: 'staging', region: 'us-east' },
});
child.increment('metric', 1);
});
server.on('metrics', metrics => {
assert.ok(metrics.includes('env:staging'));
assert.ok(metrics.includes('service:api'));
assert.ok(metrics.includes('region:us-east'));
assert.ok(!metrics.includes('env:prod'));
done();
});
});

it('should create child with no options and inherit parent config', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
prefix: 'parent.',
globalTags: ['from:parent'],
}), clientType);
statsd = parent;
const child = parent.childClient();
child.increment('metric', 1);
});
server.on('metrics', metrics => {
assert.ok(metrics.includes('parent.metric:1|c'));
assert.ok(metrics.includes('from:parent'));
done();
});
});

it('should inherit mock mode from parent', () => {
server = null;

statsd = new StatsD({ mock: true });
const child = statsd.childClient({ prefix: 'child.' });

assert.strictEqual(child.mock, true);
child.increment('metric', 1);
assert.ok(child.mockBuffer.length > 0);
assert.ok(child.mockBuffer[0].includes('child.metric:1|c'));
});

it('should inherit telegraf mode from parent', () => {
server = null;

statsd = new StatsD({ mock: true, telegraf: true });
const child = statsd.childClient({ prefix: 'child.' });

assert.strictEqual(child.telegraf, true);
});

it('should allow child to override errorHandler', done => {
server = createServer(serverType, opts => {
const parent = createHotShotsClient(Object.assign(opts, {
errorHandler() {
// parent handler - should not be called
assert.fail('parent errorHandler should not be called');
}
}), clientType);
statsd = parent;
const err = new Error('test error');
const child = parent.childClient({
errorHandler(e) {
assert.strictEqual(e, err);
done();
}
});
child.dnsError = err;
child.send('test');
});
});
});
});
Loading