Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
101 changes: 101 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 Down Expand Up @@ -159,4 +160,104 @@ 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 => {
let clock;

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);

clock.restore();
clock = null;
done();
});
});
});
});
27 changes: 27 additions & 0 deletions test/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ 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);
});
server.on('metrics', 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) {
done();
}
});
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can call done() more than once: once expectedStatuses is empty, every subsequent metrics event will still satisfy expectedStatuses.length === 0 and re-invoke done(), which fails the test run. Add a guard (e.g., a doneCalled flag) and/or switch to server.once and remove the listener after completion.

Copilot uses AI. Check for mistakes.
});
});
});
});
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');
});
});
});
});
39 changes: 39 additions & 0 deletions test/close.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,45 @@ describe('#close', () => {
});
});
});

it('should force close after 10 attempts when messagesInFlight stays positive', done => {
server = createServer(serverType, opts => {
statsd = createHotShotsClient(Object.assign(opts, {
closingFlushInterval: 5,
}), clientType);

// Simulate stuck messages in flight
statsd.messagesInFlight = 5;

statsd.close(() => {
// The force close resets messagesInFlight to 0
assert.strictEqual(statsd.messagesInFlight, 0);
server.close();
done();
});
});
});

it('should close with telemetry enabled', done => {
server = createServer(serverType, opts => {
statsd = createHotShotsClient(Object.assign(opts, {
includeDatadogTelemetry: true,
telemetryFlushInterval: 60000,
}), clientType);

statsd.increment('test.counter');
assert.ok(statsd.telemetry !== null);

let calledDone = false;
statsd.close(() => {
if (!calledDone) {
calledDone = true;
server.close();
done();
}
});
});
});
});
});
});
Loading