From 630019a54836baaad4b64af1317ca7bc3377c5f6 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:45:57 -0400 Subject: [PATCH 01/22] create dogstatsd class and constructor --- src/metrics/dogstatsd.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/metrics/dogstatsd.ts diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts new file mode 100644 index 00000000..db4f8718 --- /dev/null +++ b/src/metrics/dogstatsd.ts @@ -0,0 +1,38 @@ +import * as dgram from "node:dgram"; +import { SocketType } from "node:dgram"; + +export class LambdaDogStatsD { + private static readonly MIN_SEND_BUFFER_SIZE = 32 * 1024; + private static readonly SOCKET_TYPE: SocketType = "udp4"; + + private readonly socket: dgram.Socket; + + constructor() { + this.socket = dgram.createSocket(LambdaDogStatsD.SOCKET_TYPE); + LambdaDogStatsD.ensureMinSendBufferSize(this.socket); + } + + private static ensureMinSendBufferSize(sock: dgram.Socket): void { + if (process.platform === "win32") { + return; + } + + try { + const currentSize = sock.getSendBufferSize(); + if (currentSize <= LambdaDogStatsD.MIN_SEND_BUFFER_SIZE) { + sock.setSendBufferSize(LambdaDogStatsD.MIN_SEND_BUFFER_SIZE); + console.debug(`Socket send buffer increased to ${LambdaDogStatsD.MIN_SEND_BUFFER_SIZE / 1024}kb`); + } + } catch { + // ignore + } + } + + /** + * Send a distribution value, optionally setting tags and timestamp. + * Timestamp is seconds since epoch. + */ + public distribution(metric: string, value: number, tags?: string[], timestamp?: number): void { + // TODO + } +} From 567b5082823456886d87979be54f321201e58f5c Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:48:18 -0400 Subject: [PATCH 02/22] create `report` method to build dogstatsd payload --- src/metrics/dogstatsd.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index db4f8718..681c7f25 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -4,6 +4,8 @@ import { SocketType } from "node:dgram"; export class LambdaDogStatsD { private static readonly MIN_SEND_BUFFER_SIZE = 32 * 1024; private static readonly SOCKET_TYPE: SocketType = "udp4"; + private static readonly TAG_RE = /[^\w\d_\-:\/\.]/gu; + private static readonly TAG_SUB = "_"; private readonly socket: dgram.Socket; @@ -33,6 +35,24 @@ export class LambdaDogStatsD { * Timestamp is seconds since epoch. */ public distribution(metric: string, value: number, tags?: string[], timestamp?: number): void { + this.report(metric, "d", value, tags, timestamp); + } + + private normalizeTags(tags: string[]): string[] { + return tags.map((t) => t.replace(LambdaDogStatsD.TAG_RE, LambdaDogStatsD.TAG_SUB)); + } + + private report(metric: string, metricType: string, value: number | null, tags?: string[], timestamp?: number): void { + if (value == null) { + return; + } + const serializedTags = tags && tags.length ? `|#${this.normalizeTags(tags).join(",")}` : ""; + const timeStampPart = timestamp != null ? `|T${timestamp}` : ""; + const payload = `${metric}:${value}|${metricType}${serializedTags}${timeStampPart}`; + this.send(payload); + } + + private send(packet: string) { // TODO } } From 2dde47aca52ae2a64a2b054ae293b5b8975ad677 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:50:17 -0400 Subject: [PATCH 03/22] implement `send` UDP datagram --- src/metrics/dogstatsd.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 681c7f25..9d64c548 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -2,7 +2,10 @@ import * as dgram from "node:dgram"; import { SocketType } from "node:dgram"; export class LambdaDogStatsD { + private static readonly HOST = "localhost"; + private static readonly PORT = 8125; private static readonly MIN_SEND_BUFFER_SIZE = 32 * 1024; + private static readonly ENCODING: BufferEncoding = "utf8"; private static readonly SOCKET_TYPE: SocketType = "udp4"; private static readonly TAG_RE = /[^\w\d_\-:\/\.]/gu; private static readonly TAG_SUB = "_"; @@ -53,6 +56,14 @@ export class LambdaDogStatsD { } private send(packet: string) { - // TODO + const msg = Buffer.from(packet, LambdaDogStatsD.ENCODING); + this.socket.send(msg, LambdaDogStatsD.PORT, LambdaDogStatsD.HOST, (err) => { + if (err) { + // TODO error handling + console.log("[temp] err name:", err?.name); + console.log("[temp] err cause:", err?.cause); + console.log("[temp] err message:", err?.message); + } + }); } } From 7bb39987cedbf30d2b1f9c2efcdc9a7268e092f7 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:51:47 -0400 Subject: [PATCH 04/22] replace `hot-shots` with custom `dogstatsd` implementation --- src/metrics/listener.ts | 58 ++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index 49cfa7c1..d99bbaad 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -1,4 +1,3 @@ -import { StatsD } from "hot-shots"; import { promisify } from "util"; import { logDebug, logError, logWarning } from "../utils"; import { flushExtension, isExtensionRunning } from "./extension"; @@ -7,7 +6,7 @@ import { writeMetricToStdout } from "./metric-log"; import { Distribution } from "./model"; import { Context } from "aws-lambda"; import { getEnhancedMetricTags } from "./enhanced-metrics"; -import { SecretsManagerClientConfig } from "@aws-sdk/client-secrets-manager"; +import { LambdaDogStatsD } from "./dogstatsd"; const METRICS_BATCH_SEND_INTERVAL = 10000; // 10 seconds const HISTORICAL_METRICS_THRESHOLD_HOURS = 4 * 60 * 60 * 1000; // 4 hours @@ -64,13 +63,14 @@ export interface MetricsConfig { export class MetricsListener { private currentProcessor?: Promise; private apiKey: Promise; - private statsDClient?: StatsD; + private statsDClient: LambdaDogStatsD; private isExtensionRunning?: boolean = undefined; private globalTags?: string[] = []; constructor(private kmsClient: KMSService, private config: MetricsConfig) { this.apiKey = this.getAPIKey(config); this.config = config; + this.statsDClient = new LambdaDogStatsD(); } public async onStartInvocation(_: any, context?: Context) { @@ -83,8 +83,6 @@ export class MetricsListener { logDebug(`Using StatsD client`); this.globalTags = this.getGlobalTags(context); - // About 200 chars per metric, so 8KB buffer size holds approx 40 metrics per request - this.statsDClient = new StatsD({ host: "127.0.0.1", closingFlushInterval: 1, maxBufferSize: 8192 }); return; } if (this.config.logForwarding) { @@ -109,19 +107,9 @@ export class MetricsListener { await processor.flush(); } - if (this.statsDClient !== undefined) { + if (this.isExtensionRunning) { logDebug(`Flushing statsD`); - - // Make sure all stats are flushed to extension - await new Promise((resolve, reject) => { - this.statsDClient?.close((error) => { - if (error !== undefined) { - reject(error); - } - resolve(); - }); - }); - this.statsDClient = undefined; + // TODO } } catch (error) { // This can fail for a variety of reasons, from the API not being reachable, @@ -146,33 +134,29 @@ export class MetricsListener { forceAsync: boolean, ...tags: string[] ) { + // If extension is running, use dogstatsd (FIPS compliant) if (this.isExtensionRunning) { - const isMetricTimeValid = Date.parse(metricTime.toString()) > 0; - if (isMetricTimeValid) { - const dateCeiling = new Date(Date.now() - HISTORICAL_METRICS_THRESHOLD_HOURS); // 4 hours ago - if (dateCeiling > metricTime) { - logWarning(`Timestamp ${metricTime.toISOString()} is older than 4 hours, not submitting metric ${name}`); - return; - } - // Only create the processor to submit metrics to the API when a user provides a valid timestamp as - // Dogstatsd does not support timestamps for distributions. - if (this.currentProcessor === undefined) { - this.currentProcessor = this.createProcessor(this.config, this.apiKey); - } - // Add global tags to metrics sent to the API - if (this.globalTags !== undefined && this.globalTags.length > 0) { - tags = [...tags, ...this.globalTags]; - } - } else { - this.statsDClient?.distribution(name, value, undefined, tags); + const dateCeiling = new Date(Date.now() - HISTORICAL_METRICS_THRESHOLD_HOURS); // 4 hours ago + if (dateCeiling > metricTime) { + logWarning(`Timestamp ${metricTime.toISOString()} is older than 4 hours, not submitting metric ${name}`); return; } + + this.statsDClient?.distribution(name, value, tags, metricTime.getSeconds()); + return; } + + // If no extension + logForwarding, write to stdout (FIPS compliant) if (this.config.logForwarding || forceAsync) { writeMetricToStdout(name, value, metricTime, tags); return; } + // Otherwise, send directly to DD API (not FIPs compliant!) + // Add global tags to metrics sent to the API + if (this.globalTags !== undefined && this.globalTags.length > 0) { + tags = [...tags, ...this.globalTags]; + } const dist = new Distribution(name, [{ timestamp: metricTime, value }], ...tags); if (!this.apiKey) { @@ -191,9 +175,7 @@ export class MetricsListener { } public sendDistributionMetric(name: string, value: number, forceAsync: boolean, ...tags: string[]) { - // The Extension doesn't support distribution metrics with timestamps. Use sendDistributionMetricWithDate instead. - const metricTime = this.isExtensionRunning ? new Date(0) : new Date(Date.now()); - this.sendDistributionMetricWithDate(name, value, metricTime, forceAsync, ...tags); + this.sendDistributionMetricWithDate(name, value, new Date(), forceAsync, ...tags); } private async createProcessor(config: MetricsConfig, apiKey: Promise) { From 9dccc5dc493e74ae91a06ee0e7f4a5d6c3970b12 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:55:25 -0400 Subject: [PATCH 05/22] implement flushing strategy --- src/metrics/dogstatsd.ts | 28 +++++++++++++++++++++------- src/metrics/listener.ts | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 9d64c548..725f8d60 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -11,6 +11,7 @@ export class LambdaDogStatsD { private static readonly TAG_SUB = "_"; private readonly socket: dgram.Socket; + private readonly pendingSends = new Set>(); constructor() { this.socket = dgram.createSocket(LambdaDogStatsD.SOCKET_TYPE); @@ -57,13 +58,26 @@ export class LambdaDogStatsD { private send(packet: string) { const msg = Buffer.from(packet, LambdaDogStatsD.ENCODING); - this.socket.send(msg, LambdaDogStatsD.PORT, LambdaDogStatsD.HOST, (err) => { - if (err) { - // TODO error handling - console.log("[temp] err name:", err?.name); - console.log("[temp] err cause:", err?.cause); - console.log("[temp] err message:", err?.message); - } + const promise = new Promise((resolve) => { + this.socket.send(msg, LambdaDogStatsD.PORT, LambdaDogStatsD.HOST, (err) => { + if (err) { + // TODO error handling + console.log("[temp] err name:", err?.name); + console.log("[temp] err cause:", err?.cause); + console.log("[temp] err message:", err?.message); + } + + resolve(); + }); }); + + this.pendingSends.add(promise); + promise.finally(() => this.pendingSends.delete(promise)); + } + + /** Block until all in-flight sends have settled */ + public async flush(): Promise { + await Promise.allSettled(this.pendingSends); + this.pendingSends.clear(); } } diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index d99bbaad..9fcc159f 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -109,7 +109,7 @@ export class MetricsListener { } if (this.isExtensionRunning) { logDebug(`Flushing statsD`); - // TODO + await this.statsDClient.flush(); } } catch (error) { // This can fail for a variety of reasons, from the API not being reachable, From e1a206bd3970c51c93702bd74adcaecf4ee1360b Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:56:01 -0400 Subject: [PATCH 06/22] remove `hot-shots` --- package.json | 1 - yarn.lock | 32 -------------------------------- 2 files changed, 33 deletions(-) diff --git a/package.json b/package.json index 1bf0563f..1eec5b34 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "dependencies": { "@aws-crypto/sha256-js": "5.2.0", "dc-polyfill": "^0.1.3", - "hot-shots": "8.5.0", "promise-retry": "^2.0.1", "serialize-error": "^8.1.0", "shimmer": "1.2.1" diff --git a/yarn.lock b/yarn.lock index d5dc60d4..db10bc96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2568,13 +2568,6 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -3066,11 +3059,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -3218,13 +3206,6 @@ hasown@^2.0.0, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hot-shots@8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/hot-shots/-/hot-shots-8.5.0.tgz#860246a3694dfb74cfe6045501eb59fb57e16eb9" - integrity sha512-GNXtNSxa9qibcPhi3gndyN5g14iBJS+/DDlu7hjSPfXYJy9/fcO13DgSyfPUVWrD/aIyPY36z7MksHvDe05zYg== - optionalDependencies: - unix-dgram "2.0.x" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -4088,11 +4069,6 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nan@^2.16.0: - version "2.22.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" - integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -4778,14 +4754,6 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== -unix-dgram@2.0.x: - version "2.0.6" - resolved "https://registry.yarnpkg.com/unix-dgram/-/unix-dgram-2.0.6.tgz#6d567b0eb6d7a9504e561532b598a46e34c5968b" - integrity sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg== - dependencies: - bindings "^1.5.0" - nan "^2.16.0" - update-browserslist-db@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" From ed3f52fea0b1fd2246d0ef28c7b2826f4da018fa Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 12:59:59 -0400 Subject: [PATCH 07/22] lint --- src/metrics/dogstatsd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 725f8d60..e64b52be 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -72,7 +72,7 @@ export class LambdaDogStatsD { }); this.pendingSends.add(promise); - promise.finally(() => this.pendingSends.delete(promise)); + void promise.finally(() => this.pendingSends.delete(promise)); } /** Block until all in-flight sends have settled */ From 20da437015ab52aea7026d423be0b09601a70f9e Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 13:25:33 -0400 Subject: [PATCH 08/22] fix metric timestamp --- src/metrics/listener.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index 9fcc159f..5a3d1ef3 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -142,7 +142,8 @@ export class MetricsListener { return; } - this.statsDClient?.distribution(name, value, tags, metricTime.getSeconds()); + const secondsSinceEpoch = Math.floor(metricTime.getTime() / 1000); + this.statsDClient?.distribution(name, value, tags, secondsSinceEpoch); return; } From 7d484ba931ae8fbee47ce86e88e8f9c51dd23e53 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 13:25:49 -0400 Subject: [PATCH 09/22] set max flush timeout to prevent infinite blocking --- src/metrics/dogstatsd.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index e64b52be..a3af767c 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -9,6 +9,8 @@ export class LambdaDogStatsD { private static readonly SOCKET_TYPE: SocketType = "udp4"; private static readonly TAG_RE = /[^\w\d_\-:\/\.]/gu; private static readonly TAG_SUB = "_"; + // The maximum amount to wait while flushing pending sends, so we don't block forever. + private static readonly MAX_FLUSH_TIMEOUT = 1000; private readonly socket: dgram.Socket; private readonly pendingSends = new Set>(); @@ -77,7 +79,10 @@ export class LambdaDogStatsD { /** Block until all in-flight sends have settled */ public async flush(): Promise { - await Promise.allSettled(this.pendingSends); + const allSettled = Promise.allSettled(this.pendingSends); + const maxTimeout = new Promise((resolve) => setTimeout(resolve, LambdaDogStatsD.MAX_FLUSH_TIMEOUT)); + + await Promise.race([allSettled, maxTimeout]); this.pendingSends.clear(); } } From 85c2c18701ba2c858d031d1ab69d67ba226f9dcc Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 13:30:36 -0400 Subject: [PATCH 10/22] fix metric log using fractional timestamps --- src/metrics/metric-log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metrics/metric-log.ts b/src/metrics/metric-log.ts index 1701d6dc..50288b26 100644 --- a/src/metrics/metric-log.ts +++ b/src/metrics/metric-log.ts @@ -2,7 +2,7 @@ export function buildMetricLog(name: string, value: number, metricTime: Date, tags: string[]) { return `${JSON.stringify({ // Date.now() returns Unix time in milliseconds, we convert to seconds for DD API submission - e: metricTime.getTime() / 1000, + e: Math.floor(metricTime.getTime() / 1000), m: name, t: tags, v: value, From 6258126c0889221659ea6f048fcba3883c0d6dad Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 13:56:49 -0400 Subject: [PATCH 11/22] fix unit tests; test that we use dogstatsd with timestamp+extension --- src/metrics/dogstatsd.ts | 2 +- src/metrics/listener.spec.ts | 38 ++++++++++++++++++++-------------- src/metrics/listener.ts | 2 +- src/metrics/metric-log.spec.ts | 4 ++-- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index a3af767c..472bccc3 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -40,7 +40,7 @@ export class LambdaDogStatsD { * Send a distribution value, optionally setting tags and timestamp. * Timestamp is seconds since epoch. */ - public distribution(metric: string, value: number, tags?: string[], timestamp?: number): void { + public distribution(metric: string, value: number, timestamp?: number, tags?: string[]): void { this.report(metric, "d", value, tags, timestamp); } diff --git a/src/metrics/listener.spec.ts b/src/metrics/listener.spec.ts index bcbc2deb..8cb9b1b8 100644 --- a/src/metrics/listener.spec.ts +++ b/src/metrics/listener.spec.ts @@ -5,9 +5,10 @@ import { LogLevel, setLogLevel } from "../utils"; import { EXTENSION_URL } from "./extension"; import { MetricsListener } from "./listener"; -import StatsDClient from "hot-shots"; +import { LambdaDogStatsD } from "./dogstatsd"; import { Context } from "aws-lambda"; -jest.mock("hot-shots"); + +jest.mock("./dogstatsd"); jest.mock("@aws-sdk/client-secrets-manager", () => { return { @@ -17,6 +18,9 @@ jest.mock("@aws-sdk/client-secrets-manager", () => { }; }); +const MOCK_TIME_SECONDS = 1487076708; +const MOCK_TIME_MS = 1487076708000; + const siteURL = "example.com"; class MockKMS { @@ -56,6 +60,7 @@ describe("MetricsListener", () => { expect(nock.isDone()).toBeTruthy(); }); + it("uses encrypted kms key if it's the only value available", async () => { nock("https://api.example.com").post("/api/v1/distribution_points?api_key=kms-api-key-decrypted").reply(200, {}); @@ -184,7 +189,7 @@ describe("MetricsListener", () => { it("logs metrics when logForwarding is enabled", async () => { const spy = jest.spyOn(process.stdout, "write"); - jest.spyOn(Date, "now").mockImplementation(() => 1487076708000); + jest.spyOn(Date.prototype, "getTime").mockReturnValue(MOCK_TIME_MS); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { apiKey: "api-key", @@ -202,22 +207,23 @@ describe("MetricsListener", () => { listener.sendDistributionMetric("my-metric", 10, false, "tag:a", "tag:b"); await listener.onCompleteInvocation(); - expect(spy).toHaveBeenCalledWith(`{"e":1487076708,"m":"my-metric","t":["tag:a","tag:b"],"v":10}\n`); + expect(spy).toHaveBeenCalledWith(`{"e":${MOCK_TIME_SECONDS},"m":"my-metric","t":["tag:a","tag:b"],"v":10}\n`); }); + it("always sends metrics to statsD when extension is enabled, ignoring logForwarding=true", async () => { const flushScope = nock(EXTENSION_URL).post("/lambda/flush", JSON.stringify({})).reply(200); mock({ "/opt/extensions/datadog-agent": Buffer.from([0]), }); const distributionMock = jest.fn(); - (StatsDClient as any).mockImplementation(() => { + (LambdaDogStatsD as any).mockImplementation(() => { return { distribution: distributionMock, close: (callback: any) => callback(undefined), }; }); - jest.spyOn(Date, "now").mockImplementation(() => 1487076708000); + jest.spyOn(Date.prototype, "getTime").mockReturnValue(MOCK_TIME_MS); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { @@ -236,10 +242,11 @@ describe("MetricsListener", () => { listener.sendDistributionMetric("my-metric", 10, false, "tag:a", "tag:b"); await listener.onCompleteInvocation(); expect(flushScope.isDone()).toBeTruthy(); - expect(distributionMock).toHaveBeenCalledWith("my-metric", 10, undefined, ["tag:a", "tag:b"]); + expect(distributionMock).toHaveBeenCalledWith("my-metric", 10, MOCK_TIME_SECONDS, ["tag:a", "tag:b"]); }); - it("only sends metrics with timestamps to the API when the extension is enabled", async () => { + it("sends metrics with timestamps to statsD (not API!) when the extension is enabled", async () => { + jest.spyOn(Date.prototype, "getTime").mockReturnValue(MOCK_TIME_MS); const flushScope = nock(EXTENSION_URL).post("/lambda/flush", JSON.stringify({})).reply(200); mock({ "/opt/extensions/datadog-agent": Buffer.from([0]), @@ -247,14 +254,14 @@ describe("MetricsListener", () => { const apiScope = nock("https://api.example.com").post("/api/v1/distribution_points?api_key=api-key").reply(200, {}); const distributionMock = jest.fn(); - (StatsDClient as any).mockImplementation(() => { + (LambdaDogStatsD as any).mockImplementation(() => { return { distribution: distributionMock, close: (callback: any) => callback(undefined), }; }); - const metricTimeOneMinuteAgo = new Date(Date.now() - 60000); + const metricTimeOneMinuteAgo = new Date(MOCK_TIME_MS - 60000); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { apiKey: "api-key", @@ -280,12 +287,15 @@ describe("MetricsListener", () => { "tag:a", "tag:b", ); - listener.sendDistributionMetric("my-metric-without-a-timestamp", 10, false, "tag:a", "tag:b"); + listener.sendDistributionMetric("my-metric-with-a-timestamp", 10, false, "tag:a", "tag:b"); await listener.onCompleteInvocation(); expect(flushScope.isDone()).toBeTruthy(); - expect(apiScope.isDone()).toBeTruthy(); - expect(distributionMock).toHaveBeenCalledWith("my-metric-without-a-timestamp", 10, undefined, ["tag:a", "tag:b"]); + expect(apiScope.isDone()).toBeFalsy(); + expect(distributionMock).toHaveBeenCalledWith("my-metric-with-a-timestamp", 10, MOCK_TIME_SECONDS, [ + "tag:a", + "tag:b", + ]); }); it("does not send historical metrics from over 4 hours ago to the API", async () => { @@ -316,7 +326,6 @@ describe("MetricsListener", () => { it("logs metrics when logForwarding is enabled with custom timestamp", async () => { const spy = jest.spyOn(process.stdout, "write"); - // jest.spyOn(Date, "now").mockImplementation(() => 1487076708000); const kms = new MockKMS("kms-api-key-decrypted"); const listener = new MetricsListener(kms as any, { apiKey: "api-key", @@ -328,7 +337,6 @@ describe("MetricsListener", () => { localTesting: false, siteURL, }); - // jest.useFakeTimers(); await listener.onStartInvocation({}); listener.sendDistributionMetricWithDate("my-metric", 10, new Date(1584983836 * 1000), false, "tag:a", "tag:b"); diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index 5a3d1ef3..30b28e42 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -143,7 +143,7 @@ export class MetricsListener { } const secondsSinceEpoch = Math.floor(metricTime.getTime() / 1000); - this.statsDClient?.distribution(name, value, tags, secondsSinceEpoch); + this.statsDClient?.distribution(name, value, secondsSinceEpoch, tags); return; } diff --git a/src/metrics/metric-log.spec.ts b/src/metrics/metric-log.spec.ts index e3c9107d..7b5f51b4 100644 --- a/src/metrics/metric-log.spec.ts +++ b/src/metrics/metric-log.spec.ts @@ -3,14 +3,14 @@ import { buildMetricLog } from "./metric-log"; describe("buildMetricLog", () => { it("handles empty tag list", () => { expect(buildMetricLog("my.test.metric", 1337, new Date(1487076708123), [])).toStrictEqual( - '{"e":1487076708.123,"m":"my.test.metric","t":[],"v":1337}\n', + '{"e":1487076708,"m":"my.test.metric","t":[],"v":1337}\n', ); }); it("writes timestamp in Unix seconds", () => { expect( buildMetricLog("my.test.metric", 1337, new Date(1487076708123), ["region:us", "account:dev", "team:serverless"]), ).toStrictEqual( - '{"e":1487076708.123,"m":"my.test.metric","t":["region:us","account:dev","team:serverless"],"v":1337}\n', + '{"e":1487076708,"m":"my.test.metric","t":["region:us","account:dev","team:serverless"],"v":1337}\n', ); }); }); From 79bdb436e0dc28879f4d77419fe9d0537a32fcec Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 13:57:01 -0400 Subject: [PATCH 12/22] update license --- LICENSE-3rdparty.csv | 1 - 1 file changed, 1 deletion(-) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 428a44c6..01f17ccc 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -15,7 +15,6 @@ ts-jest,dev,MIT,Copyright (c) 2016-2018 Kulshekhar Kabra tslint,dev,Apache-2.0,"Copyright 2013-2019 Palantir Technologies, Inc." typescript,dev,Apache-2.0,Copyright (c) Microsoft Corporation. dc-polyfill,import,MIT,"Copyright (c) 2023 Datadog, Inc." -hot-shots,import,MIT,Copyright 2011 Steve Ivy. All rights reserved. promise-retry,import,MIT,Copyright (c) 2014 IndigoUnited serialize-error,import,MIT,Copyright (c) Sindre Sorhus (https://sindresorhus.com) shimmer,import,BSD-2-Clause,"Copyright (c) 2013-2019, Forrest L Norvell" From 1a2a38a4d896628e8f21418132b554331b91a7e4 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 14:13:39 -0400 Subject: [PATCH 13/22] add debug log on error --- src/metrics/dogstatsd.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 472bccc3..5791513d 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -63,10 +63,7 @@ export class LambdaDogStatsD { const promise = new Promise((resolve) => { this.socket.send(msg, LambdaDogStatsD.PORT, LambdaDogStatsD.HOST, (err) => { if (err) { - // TODO error handling - console.log("[temp] err name:", err?.name); - console.log("[temp] err cause:", err?.cause); - console.log("[temp] err message:", err?.message); + console.debug(`Unable to send metric packet: ${err.message}`); } resolve(); From f1f361516ba55bb2753a358f6f0916371d1c1bd6 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 14:16:53 -0400 Subject: [PATCH 14/22] unit tests --- src/metrics/dogstatsd.spec.ts | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/metrics/dogstatsd.spec.ts diff --git a/src/metrics/dogstatsd.spec.ts b/src/metrics/dogstatsd.spec.ts new file mode 100644 index 00000000..deb1bc3d --- /dev/null +++ b/src/metrics/dogstatsd.spec.ts @@ -0,0 +1,72 @@ +import * as dgram from "node:dgram"; +import { LambdaDogStatsD } from "./dogstatsd"; + +jest.mock("node:dgram", () => ({ + createSocket: jest.fn(), +})); + +describe("LambdaDogStatsD", () => { + let mockSend: jest.Mock; + + beforeEach(() => { + // A send() that immediately calls its callback + mockSend = jest.fn((msg, port, host, cb) => cb()); + (dgram.createSocket as jest.Mock).mockReturnValue({ + send: mockSend, + getSendBufferSize: jest.fn().mockReturnValue(64 * 1024), + setSendBufferSize: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sends a distribution metric without tags or timestamp", async () => { + const client = new LambdaDogStatsD(); + client.distribution("metric", 1); + await client.flush(); + + expect(mockSend).toHaveBeenCalledWith(Buffer.from("metric:1|d", "utf8"), 8125, "localhost", expect.any(Function)); + }); + + it("sends with tags (sanitized) and timestamp", async () => { + const client = new LambdaDogStatsD(); + client.distribution("metric2", 2, 12345, ["tag1", "bad?tag"]); + await client.flush(); + + // "bad?tag" becomes "bad_tag" + expect(mockSend).toHaveBeenCalledWith( + Buffer.from("metric2:2|d|#tag1,bad_tag|T12345", "utf8"), + 8125, + "localhost", + expect.any(Function), + ); + }); + + it("flush() resolves immediately when there are no sends", async () => { + const client = new LambdaDogStatsD(); + await expect(client.flush()).resolves.toBeUndefined(); + }); + + it("flush() times out if a send never invokes its callback", async () => { + // replace socket.send with a never‐calling callback + (dgram.createSocket as jest.Mock).mockReturnValue({ + send: jest.fn(), // never calls callback + getSendBufferSize: jest.fn(), + setSendBufferSize: jest.fn(), + }); + + const client = new LambdaDogStatsD(); + client.distribution("will", 9); + + jest.useFakeTimers(); + const p = client.flush(); + // advance past the 1000ms MAX_FLUSH_TIMEOUT + jest.advanceTimersByTime(1100); + + // expect the Promise returned by flush() to resolve successfully + await expect(p).resolves.toBeUndefined(); + jest.useRealTimers(); + }); +}); From e0fa27c668e7bb83c62dab48161340c1707e0fc5 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 14:33:36 -0400 Subject: [PATCH 15/22] fix debug logs --- src/metrics/dogstatsd.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 5791513d..44981041 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -1,5 +1,6 @@ import * as dgram from "node:dgram"; import { SocketType } from "node:dgram"; +import { logDebug } from "../utils"; export class LambdaDogStatsD { private static readonly HOST = "localhost"; @@ -29,7 +30,7 @@ export class LambdaDogStatsD { const currentSize = sock.getSendBufferSize(); if (currentSize <= LambdaDogStatsD.MIN_SEND_BUFFER_SIZE) { sock.setSendBufferSize(LambdaDogStatsD.MIN_SEND_BUFFER_SIZE); - console.debug(`Socket send buffer increased to ${LambdaDogStatsD.MIN_SEND_BUFFER_SIZE / 1024}kb`); + logDebug(`Socket send buffer increased to ${LambdaDogStatsD.MIN_SEND_BUFFER_SIZE / 1024}kb`); } } catch { // ignore @@ -63,7 +64,7 @@ export class LambdaDogStatsD { const promise = new Promise((resolve) => { this.socket.send(msg, LambdaDogStatsD.PORT, LambdaDogStatsD.HOST, (err) => { if (err) { - console.debug(`Unable to send metric packet: ${err.message}`); + logDebug(`Unable to send metric packet: ${err.message}`); } resolve(); From 48eb47d5fbf4366bd90b9a39ad0f1974ddebce9e Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 7 May 2025 15:00:44 -0400 Subject: [PATCH 16/22] log when socket times out --- src/metrics/dogstatsd.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 44981041..7b7358ec 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -78,9 +78,15 @@ export class LambdaDogStatsD { /** Block until all in-flight sends have settled */ public async flush(): Promise { const allSettled = Promise.allSettled(this.pendingSends); - const maxTimeout = new Promise((resolve) => setTimeout(resolve, LambdaDogStatsD.MAX_FLUSH_TIMEOUT)); + const maxTimeout = new Promise<"timeout">((resolve) => { + setTimeout(() => resolve("timeout"), LambdaDogStatsD.MAX_FLUSH_TIMEOUT); + }); + + const winner = await Promise.race([allSettled, maxTimeout]); + if (winner === "timeout") { + logDebug("Timed out before sending all metric payloads"); + } - await Promise.race([allSettled, maxTimeout]); this.pendingSends.clear(); } } From 5ea40f6f08388d670de2c2595380bd4aa976ec1c Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Thu, 8 May 2025 10:28:40 -0400 Subject: [PATCH 17/22] nits --- src/metrics/dogstatsd.spec.ts | 4 ++-- src/metrics/dogstatsd.ts | 6 +++--- src/metrics/listener.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/metrics/dogstatsd.spec.ts b/src/metrics/dogstatsd.spec.ts index deb1bc3d..5611c2f5 100644 --- a/src/metrics/dogstatsd.spec.ts +++ b/src/metrics/dogstatsd.spec.ts @@ -27,7 +27,7 @@ describe("LambdaDogStatsD", () => { client.distribution("metric", 1); await client.flush(); - expect(mockSend).toHaveBeenCalledWith(Buffer.from("metric:1|d", "utf8"), 8125, "localhost", expect.any(Function)); + expect(mockSend).toHaveBeenCalledWith(Buffer.from("metric:1|d", "utf8"), 8125, "127.0.0.1", expect.any(Function)); }); it("sends with tags (sanitized) and timestamp", async () => { @@ -39,7 +39,7 @@ describe("LambdaDogStatsD", () => { expect(mockSend).toHaveBeenCalledWith( Buffer.from("metric2:2|d|#tag1,bad_tag|T12345", "utf8"), 8125, - "localhost", + "127.0.0.1", expect.any(Function), ); }); diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 7b7358ec..93c81637 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -3,7 +3,7 @@ import { SocketType } from "node:dgram"; import { logDebug } from "../utils"; export class LambdaDogStatsD { - private static readonly HOST = "localhost"; + private static readonly HOST = "127.0.0.1"; private static readonly PORT = 8125; private static readonly MIN_SEND_BUFFER_SIZE = 32 * 1024; private static readonly ENCODING: BufferEncoding = "utf8"; @@ -54,8 +54,8 @@ export class LambdaDogStatsD { return; } const serializedTags = tags && tags.length ? `|#${this.normalizeTags(tags).join(",")}` : ""; - const timeStampPart = timestamp != null ? `|T${timestamp}` : ""; - const payload = `${metric}:${value}|${metricType}${serializedTags}${timeStampPart}`; + const timestampPart = timestamp != null ? `|T${timestamp}` : ""; + const payload = `${metric}:${value}|${metricType}${serializedTags}${timestampPart}`; this.send(payload); } diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index 30b28e42..f1019eba 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -143,7 +143,7 @@ export class MetricsListener { } const secondsSinceEpoch = Math.floor(metricTime.getTime() / 1000); - this.statsDClient?.distribution(name, value, secondsSinceEpoch, tags); + this.statsDClient.distribution(name, value, secondsSinceEpoch, tags); return; } From 11fe06e045255724e73967821774c99ddee60227 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Thu, 8 May 2025 11:14:24 -0400 Subject: [PATCH 18/22] round in dogstatsd; unit test --- src/metrics/dogstatsd.spec.ts | 13 +++++++++++++ src/metrics/dogstatsd.ts | 5 +++++ src/metrics/listener.ts | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/metrics/dogstatsd.spec.ts b/src/metrics/dogstatsd.spec.ts index 5611c2f5..976ca1bb 100644 --- a/src/metrics/dogstatsd.spec.ts +++ b/src/metrics/dogstatsd.spec.ts @@ -44,6 +44,19 @@ describe("LambdaDogStatsD", () => { ); }); + it("rounds timestamp", async () => { + const client = new LambdaDogStatsD(); + client.distribution("metric2", 2, 12345.678); + await client.flush(); + + expect(mockSend).toHaveBeenCalledWith( + Buffer.from("metric2:2|d|T12345", "utf8"), + 8125, + "127.0.0.1", + expect.any(Function), + ); + }); + it("flush() resolves immediately when there are no sends", async () => { const client = new LambdaDogStatsD(); await expect(client.flush()).resolves.toBeUndefined(); diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 93c81637..656a7a5b 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -53,6 +53,11 @@ export class LambdaDogStatsD { if (value == null) { return; } + + if (timestamp) { + timestamp = Math.floor(timestamp); + } + const serializedTags = tags && tags.length ? `|#${this.normalizeTags(tags).join(",")}` : ""; const timestampPart = timestamp != null ? `|T${timestamp}` : ""; const payload = `${metric}:${value}|${metricType}${serializedTags}${timestampPart}`; diff --git a/src/metrics/listener.ts b/src/metrics/listener.ts index f1019eba..bc349f0b 100644 --- a/src/metrics/listener.ts +++ b/src/metrics/listener.ts @@ -142,7 +142,7 @@ export class MetricsListener { return; } - const secondsSinceEpoch = Math.floor(metricTime.getTime() / 1000); + const secondsSinceEpoch = metricTime.getTime() / 1000; this.statsDClient.distribution(name, value, secondsSinceEpoch, tags); return; } From b77211a23775f513c823cf0568059d0b17ccca07 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Thu, 8 May 2025 11:46:01 -0400 Subject: [PATCH 19/22] bind to local port before setting min send buffer size; add debug --- src/index.spec.ts | 2 +- src/metrics/dogstatsd.spec.ts | 2 ++ src/metrics/dogstatsd.ts | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index eba64684..86e31a62 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -509,7 +509,7 @@ describe("datadog", () => { expect(mockedIncrementInvocations).toBeCalledTimes(1); expect(mockedIncrementInvocations).toBeCalledWith(expect.anything(), mockContext); - expect(logger.debug).toHaveBeenCalledTimes(8); + expect(logger.debug).toHaveBeenCalledTimes(9); expect(logger.debug).toHaveBeenLastCalledWith('{"status":"debug","message":"datadog:Unpatching HTTP libraries"}'); }); diff --git a/src/metrics/dogstatsd.spec.ts b/src/metrics/dogstatsd.spec.ts index 976ca1bb..7c656e14 100644 --- a/src/metrics/dogstatsd.spec.ts +++ b/src/metrics/dogstatsd.spec.ts @@ -15,6 +15,7 @@ describe("LambdaDogStatsD", () => { send: mockSend, getSendBufferSize: jest.fn().mockReturnValue(64 * 1024), setSendBufferSize: jest.fn(), + bind: jest.fn(), }); }); @@ -68,6 +69,7 @@ describe("LambdaDogStatsD", () => { send: jest.fn(), // never calls callback getSendBufferSize: jest.fn(), setSendBufferSize: jest.fn(), + bind: jest.fn(), }); const client = new LambdaDogStatsD(); diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 656a7a5b..9fa39620 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -18,7 +18,10 @@ export class LambdaDogStatsD { constructor() { this.socket = dgram.createSocket(LambdaDogStatsD.SOCKET_TYPE); - LambdaDogStatsD.ensureMinSendBufferSize(this.socket); + // Bind to a local port so we can set the socket’s send buffer size + this.socket.bind(0, () => { + LambdaDogStatsD.ensureMinSendBufferSize(this.socket); + }); } private static ensureMinSendBufferSize(sock: dgram.Socket): void { @@ -33,7 +36,7 @@ export class LambdaDogStatsD { logDebug(`Socket send buffer increased to ${LambdaDogStatsD.MIN_SEND_BUFFER_SIZE / 1024}kb`); } } catch { - // ignore + logDebug("Unable to set socket's send buffer size") } } From fc08ed8633fe1def5e12feed8cb95ba3e026c6e9 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Thu, 8 May 2025 11:47:27 -0400 Subject: [PATCH 20/22] lint --- src/metrics/dogstatsd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index 9fa39620..c15b78bb 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -36,7 +36,7 @@ export class LambdaDogStatsD { logDebug(`Socket send buffer increased to ${LambdaDogStatsD.MIN_SEND_BUFFER_SIZE / 1024}kb`); } } catch { - logDebug("Unable to set socket's send buffer size") + logDebug("Unable to set socket's send buffer size"); } } From fdb0beeef639cbcfd90f5139eee4e98de67b00e4 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Thu, 8 May 2025 14:26:57 -0400 Subject: [PATCH 21/22] fix --- src/index.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 86e31a62..d1b545f4 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -509,7 +509,6 @@ describe("datadog", () => { expect(mockedIncrementInvocations).toBeCalledTimes(1); expect(mockedIncrementInvocations).toBeCalledWith(expect.anything(), mockContext); - expect(logger.debug).toHaveBeenCalledTimes(9); expect(logger.debug).toHaveBeenLastCalledWith('{"status":"debug","message":"datadog:Unpatching HTTP libraries"}'); }); From b7967e608e86759d7074df66b0264cab12db70c1 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Thu, 8 May 2025 15:36:18 -0400 Subject: [PATCH 22/22] remove `setSendBufferSize`, since it sets MAX send size, not MIN send size... no way to set min send size in Node --- src/metrics/dogstatsd.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/metrics/dogstatsd.ts b/src/metrics/dogstatsd.ts index c15b78bb..4e3b6913 100644 --- a/src/metrics/dogstatsd.ts +++ b/src/metrics/dogstatsd.ts @@ -18,26 +18,6 @@ export class LambdaDogStatsD { constructor() { this.socket = dgram.createSocket(LambdaDogStatsD.SOCKET_TYPE); - // Bind to a local port so we can set the socket’s send buffer size - this.socket.bind(0, () => { - LambdaDogStatsD.ensureMinSendBufferSize(this.socket); - }); - } - - private static ensureMinSendBufferSize(sock: dgram.Socket): void { - if (process.platform === "win32") { - return; - } - - try { - const currentSize = sock.getSendBufferSize(); - if (currentSize <= LambdaDogStatsD.MIN_SEND_BUFFER_SIZE) { - sock.setSendBufferSize(LambdaDogStatsD.MIN_SEND_BUFFER_SIZE); - logDebug(`Socket send buffer increased to ${LambdaDogStatsD.MIN_SEND_BUFFER_SIZE / 1024}kb`); - } - } catch { - logDebug("Unable to set socket's send buffer size"); - } } /**