From 3c6a0de0eaf216020182eb54b2961d3106f888cd Mon Sep 17 00:00:00 2001 From: Ryan Rishi Date: Mon, 8 Apr 2024 10:24:58 -0400 Subject: [PATCH 1/4] feat(client): add support for tags --- src/index.ts | 17 ++++++++++++++ test/index.spec.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/index.ts b/src/index.ts index cc7dd4c2..db1eefe4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,8 @@ export enum Types { Object.freeze(Types); +export type Tags = { [key: string]: string } | string[]; + export interface Options { host?: string | URL; namespace?: string; @@ -30,6 +32,7 @@ export interface Options { udpDnsCache?: boolean; udpDnsCacheTTL?: number; customSocket?: Socket; + tags?: Tags; } /** @@ -49,6 +52,7 @@ class Client { protected namespace: string; protected debug: DebugLogger; protected isDebug: boolean; + protected tags: Tags; constructor({ host, @@ -60,6 +64,7 @@ class Client { debug = null, onError = () => undefined, customSocket = null, + tags = null, }: Options = {}) { if (typeof namespace !== 'string') { throw new Error('A namespace string is required'); @@ -122,6 +127,7 @@ class Client { this.bufferFlushTimeout = bufferFlushTimeout; this.timeout = null; this.timeoutActive = false; + this.tags = tags; } connect(): Promise { @@ -144,6 +150,17 @@ class Client { metric += '|@' + sampling; } + if (this.tags) { + if (Array.isArray(this.tags)) { + metric += `|#${this.tags.join(',')}`; + } else { + const tags = Object.keys(this.tags) + .map((tag) => `${tag}:${this.tags[tag]}`) + .join(','); + metric += `|#${tags}`; + } + } + return metric; } /** diff --git a/test/index.spec.ts b/test/index.spec.ts index 7fdc5780..59ef9d95 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -111,6 +111,20 @@ test('counter with sampling', (t) => { }); }); +test('counter with tags', (t) => { + const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); + const namespace = 'ns1'; + const tags = { tag1: 'value1', tag2: 'value2' }; + const client = new Client({ host, namespace, tags }); + return new Promise((resolve) => { + t.context.server.on('metric', (metric) => { + t.is(`${namespace}.some.metric:1|c|@10|#tag1:value1,tag2:value2`, metric.toString()); + return resolve(0); + }); + client.counter('some.metric', 1, 10); + }); +}); + test('timing', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; @@ -137,6 +151,20 @@ test('timing with sampling', (t) => { }); }); +test('timing with tags', (t) => { + const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); + const namespace = 'ns1'; + const tags = { tag1: 'value1', tag2: 'value2' }; + const client = new Client({ host, namespace, tags }); + return new Promise((resolve) => { + t.context.server.on('metric', (metric) => { + t.is(`${namespace}.some.metric:1|ms|@10|#tag1:value1,tag2:value2`, metric.toString()); + return resolve(0); + }); + client.timing('some.metric', 1, 10); + }); +}); + test('gauge', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; @@ -163,6 +191,20 @@ test('gauge should ignore sampling', (t) => { }); }); +test('gauge with tags', (t) => { + const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); + const namespace = 'ns1'; + const tags = { tag1: 'value1', tag2: 'value2' }; + const client = new Client({ host, namespace, tags }); + return new Promise((resolve) => { + t.context.server.on('metric', (metric) => { + t.is(`${namespace}.some.metric:1|g|#tag1:value1,tag2:value2`, metric.toString()); + return resolve(0); + }); + client.gauge('some.metric', 1); + }); +}); + test('set', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; @@ -189,6 +231,20 @@ test('set should ignore sampling', (t) => { }); }); +test('set with tags', (t) => { + const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); + const namespace = 'ns1'; + const tags = { tag1: 'value1', tag2: 'value2' }; + const client = new Client({ host, namespace, tags }); + return new Promise((resolve) => { + t.context.server.on('metric', (metric) => { + t.is(`${namespace}.some.metric:1|s|#tag1:value1,tag2:value2`, metric.toString()); + return resolve(0); + }); + client.set('some.metric', 1); + }); +}); + test.serial('hostname substitution', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1.${hostname}'; From 2e8c30a410ae6c26e96a4144ee556f2a9bc14e41 Mon Sep 17 00:00:00 2001 From: Ryan Rishi Date: Mon, 8 Apr 2024 10:38:09 -0400 Subject: [PATCH 2/4] docs: update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e37ff27..f71e0e06 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ This module exports: - `udpDnsCacheTTL`: Optional. Default `120`. Dns cache Time to live in seconds. - `onError`: Optional. Default `(err) => void`. Called when there is an error. Allows you to check also send errors. - `customSocket`: Optional. Default `null`. Custom socket used by the client, this is a feature for mocking we do not recommend using it in production. + - `tags`: Optional Default `null`. If provided, metrics will include tags in the form `#key1:value1,key2:value2`. #### `Client.close([cb])` @@ -328,5 +329,5 @@ If you have any questions on how to use dats, bugs and enhancement please feel f ## License -dats is licensed under the MIT license. +dats is licensed under the MIT license. See the [LICENSE](./LICENSE) file for more information. From d1f4915631d3bb475f11108423baafc89ddbc24e Mon Sep 17 00:00:00 2001 From: Ryan Rishi Date: Thu, 16 May 2024 11:46:41 -0400 Subject: [PATCH 3/4] feat(client): move tag serialization into constructor --- src/index.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index db1eefe4..9736cabf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,7 @@ class Client { protected namespace: string; protected debug: DebugLogger; protected isDebug: boolean; - protected tags: Tags; + protected tags: string; constructor({ host, @@ -127,7 +127,15 @@ class Client { this.bufferFlushTimeout = bufferFlushTimeout; this.timeout = null; this.timeoutActive = false; - this.tags = tags; + if (tags) { + if (Array.isArray(tags)) { + this.tags = tags.join(','); + } else { + this.tags = Object.keys(tags) + .map((tag) => `${tag}:${tags[tag]}`) + .join(','); + } + } } connect(): Promise { @@ -151,14 +159,7 @@ class Client { } if (this.tags) { - if (Array.isArray(this.tags)) { - metric += `|#${this.tags.join(',')}`; - } else { - const tags = Object.keys(this.tags) - .map((tag) => `${tag}:${this.tags[tag]}`) - .join(','); - metric += `|#${tags}`; - } + metric += `|#${this.tags}`; } return metric; From 8a753ff717790397d48cbb2bc9c79eae48152059 Mon Sep 17 00:00:00 2001 From: Ryan Rishi Date: Thu, 16 May 2024 11:51:09 -0400 Subject: [PATCH 4/4] feat(client): support simple tags --- src/index.ts | 4 ++-- test/index.spec.ts | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9736cabf..fe6a0244 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export enum Types { Object.freeze(Types); -export type Tags = { [key: string]: string } | string[]; +export type Tags = { [key: string]: string | null } | string[]; export interface Options { host?: string | URL; @@ -132,7 +132,7 @@ class Client { this.tags = tags.join(','); } else { this.tags = Object.keys(tags) - .map((tag) => `${tag}:${tags[tag]}`) + .map((tag) => (tags[tag] ? `${tag}:${tags[tag]}` : tag)) .join(','); } } diff --git a/test/index.spec.ts b/test/index.spec.ts index 59ef9d95..7509defb 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -114,11 +114,14 @@ test('counter with sampling', (t) => { test('counter with tags', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; - const tags = { tag1: 'value1', tag2: 'value2' }; + const tags = { tag1: 'value1', tag2: null, tag3: 'value3' }; const client = new Client({ host, namespace, tags }); return new Promise((resolve) => { t.context.server.on('metric', (metric) => { - t.is(`${namespace}.some.metric:1|c|@10|#tag1:value1,tag2:value2`, metric.toString()); + t.is( + `${namespace}.some.metric:1|c|@10|#tag1:value1,tag2,tag3:value3`, + metric.toString() + ); return resolve(0); }); client.counter('some.metric', 1, 10); @@ -154,11 +157,14 @@ test('timing with sampling', (t) => { test('timing with tags', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; - const tags = { tag1: 'value1', tag2: 'value2' }; + const tags = { tag1: 'value1', tag2: null, tag3: 'value3' }; const client = new Client({ host, namespace, tags }); return new Promise((resolve) => { t.context.server.on('metric', (metric) => { - t.is(`${namespace}.some.metric:1|ms|@10|#tag1:value1,tag2:value2`, metric.toString()); + t.is( + `${namespace}.some.metric:1|ms|@10|#tag1:value1,tag2,tag3:value3`, + metric.toString() + ); return resolve(0); }); client.timing('some.metric', 1, 10); @@ -194,11 +200,14 @@ test('gauge should ignore sampling', (t) => { test('gauge with tags', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; - const tags = { tag1: 'value1', tag2: 'value2' }; + const tags = { tag1: 'value1', tag2: null, tag3: 'value3' }; const client = new Client({ host, namespace, tags }); return new Promise((resolve) => { t.context.server.on('metric', (metric) => { - t.is(`${namespace}.some.metric:1|g|#tag1:value1,tag2:value2`, metric.toString()); + t.is( + `${namespace}.some.metric:1|g|#tag1:value1,tag2,tag3:value3`, + metric.toString() + ); return resolve(0); }); client.gauge('some.metric', 1); @@ -234,11 +243,14 @@ test('set should ignore sampling', (t) => { test('set with tags', (t) => { const host = new URL(`udp://127.0.0.1:${t.context.address.port}`); const namespace = 'ns1'; - const tags = { tag1: 'value1', tag2: 'value2' }; + const tags = { tag1: 'value1', tag2: null, tag3: 'value3' }; const client = new Client({ host, namespace, tags }); return new Promise((resolve) => { t.context.server.on('metric', (metric) => { - t.is(`${namespace}.some.metric:1|s|#tag1:value1,tag2:value2`, metric.toString()); + t.is( + `${namespace}.some.metric:1|s|#tag1:value1,tag2,tag3:value3`, + metric.toString() + ); return resolve(0); }); client.set('some.metric', 1);