Skip to content

Commit 211d047

Browse files
[SVLS-4999] Add Lambda tags to metrics sent via the API (#559)
* tags for API metrics * warn on timestamp * make pretty * modify test * lint * simplify + fix test * improve log message + add test * add test for tags * fix test * handle arn tag alias * convert number into constant --------- Co-authored-by: jordan gonzález <[email protected]>
1 parent 1273883 commit 211d047

File tree

5 files changed

+93
-13
lines changed

5 files changed

+93
-13
lines changed

src/index.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,18 @@ describe("sendDistributionMetricWithDate", () => {
555555
sendDistributionMetricWithDate("metric", 1, new Date(), "first-tag", "second-tag");
556556
expect(_metricsQueue.length).toBe(1);
557557
});
558+
it("attaches tags from Datadog environment variables to the metric", () => {
559+
process.env.DD_TAGS = "foo:bar,hello:world";
560+
sendDistributionMetricWithDate("metric", 1, new Date(Date.now() - 1 * 60 * 60 * 1000), "first-tag", "second-tag");
561+
expect(_metricsQueue.length).toBe(1);
562+
const metricTags = _metricsQueue.pop()?.tags;
563+
expect(metricTags).toBeDefined();
564+
["first-tag", "second-tag", `dd_lambda_layer:datadog-node${process.version}`, "foo:bar", "hello:world"].forEach(
565+
(tag) => {
566+
expect(metricTags?.indexOf(tag)).toBeGreaterThanOrEqual(0);
567+
},
568+
);
569+
});
558570
});
559571

560572
describe("emitTelemetryOnErrorOutsideHandler", () => {

src/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export function datadog<TEvent, TResult>(
153153

154154
try {
155155
await traceListener.onStartInvocation(event, context);
156-
await metricsListener.onStartInvocation(event);
156+
await metricsListener.onStartInvocation(event, context);
157157
if (finalConfig.enhancedMetrics) {
158158
incrementInvocationsMetric(metricsListener, context);
159159
}
@@ -267,7 +267,7 @@ export function extractArgs<TEvent>(isResponseStreamFunction: boolean, ...args:
267267
* @param tags The tags associated with the metric. Should be of the format "tag:value".
268268
*/
269269
export function sendDistributionMetricWithDate(name: string, value: number, metricTime: Date, ...tags: string[]) {
270-
tags = [...tags, getRuntimeTag()];
270+
tags = [...tags, getRuntimeTag(), ...getDDTags()];
271271

272272
if (currentMetricsListener !== undefined) {
273273
currentMetricsListener.sendDistributionMetricWithDate(name, value, metricTime, false, ...tags);
@@ -419,6 +419,19 @@ function getRuntimeTag(): string {
419419
return `dd_lambda_layer:datadog-node${version}`;
420420
}
421421

422+
function getDDTags(): string[] {
423+
const ddTags = getEnvValue("DD_TAGS", "").split(",");
424+
const ddService = getEnvValue("DD_SERVICE", "");
425+
if (ddService.length > 0) {
426+
ddTags.push(`service:${ddService}`);
427+
}
428+
const ddEnv = getEnvValue("DD_ENV", "");
429+
if (ddEnv.length > 0) {
430+
ddTags.push(`env:${ddEnv}`);
431+
}
432+
return ddTags;
433+
}
434+
422435
export async function emitTelemetryOnErrorOutsideHandler(
423436
error: Error,
424437
functionName: string,

src/metrics/batcher.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ export class Batcher {
2323
* Convert batched metrics to a list of api compatible metrics
2424
*/
2525
public toAPIMetrics(): APIMetric[] {
26-
return [...this.metrics.values()]
27-
.map((metric) => metric.toAPIMetrics()) // No flatMap support yet in node 10
28-
.reduce((prev, curr) => prev.concat(curr), []);
26+
return [...this.metrics.values()].flatMap((metric) => metric.toAPIMetrics());
2927
}
3028

3129
private getBatchKey(metric: Metric): string {

src/metrics/listener.spec.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EXTENSION_URL } from "./extension";
66

77
import { MetricsListener } from "./listener";
88
import StatsDClient from "hot-shots";
9+
import { Context } from "aws-lambda";
910
jest.mock("hot-shots");
1011

1112
const siteURL = "example.com";
@@ -143,7 +144,7 @@ describe("MetricsListener", () => {
143144
mock({
144145
"/opt/extensions/datadog-agent": Buffer.from([0]),
145146
});
146-
nock("https://api.example.com").post("/api/v1/distribution_points?api_key=api-key").reply(200, {});
147+
const apiScope = nock("https://api.example.com").post("/api/v1/distribution_points?api_key=api-key").reply(200, {});
147148

148149
const distributionMock = jest.fn();
149150
(StatsDClient as any).mockImplementation(() => {
@@ -153,8 +154,6 @@ describe("MetricsListener", () => {
153154
};
154155
});
155156

156-
jest.spyOn(Date, "now").mockImplementation(() => 1487076708000);
157-
158157
const metricTimeOneMinuteAgo = new Date(Date.now() - 60000);
159158
const kms = new MockKMS("kms-api-key-decrypted");
160159
const listener = new MetricsListener(kms as any, {
@@ -166,8 +165,12 @@ describe("MetricsListener", () => {
166165
localTesting: true,
167166
siteURL,
168167
});
168+
const mockARN = "arn:aws:lambda:us-east-1:123497598159:function:my-test-lambda";
169+
const mockContext = {
170+
invokedFunctionArn: mockARN,
171+
} as any as Context;
169172

170-
await listener.onStartInvocation({});
173+
await listener.onStartInvocation({}, mockContext);
171174
listener.sendDistributionMetricWithDate(
172175
"my-metric-with-a-timestamp",
173176
10,
@@ -178,11 +181,37 @@ describe("MetricsListener", () => {
178181
);
179182
listener.sendDistributionMetric("my-metric-without-a-timestamp", 10, false, "tag:a", "tag:b");
180183
await listener.onCompleteInvocation();
184+
181185
expect(flushScope.isDone()).toBeTruthy();
182-
expect(nock.isDone()).toBeTruthy();
186+
expect(apiScope.isDone()).toBeTruthy();
183187
expect(distributionMock).toHaveBeenCalledWith("my-metric-without-a-timestamp", 10, undefined, ["tag:a", "tag:b"]);
184188
});
185189

190+
it("does not send historical metrics from over 4 hours ago to the API", async () => {
191+
mock({
192+
"/opt/extensions/datadog-agent": Buffer.from([0]),
193+
});
194+
const apiScope = nock("https://api.example.com").post("/api/v1/distribution_points?api_key=api-key").reply(200, {});
195+
196+
const metricTimeFiveHoursAgo = new Date(Date.now() - 5 * 60 * 60 * 1000); // 5 hours ago
197+
const kms = new MockKMS("kms-api-key-decrypted");
198+
const listener = new MetricsListener(kms as any, {
199+
apiKey: "api-key",
200+
apiKeyKMS: "",
201+
enhancedMetrics: false,
202+
logForwarding: false,
203+
shouldRetryMetrics: false,
204+
localTesting: true,
205+
siteURL,
206+
});
207+
208+
await listener.onStartInvocation({});
209+
listener.sendDistributionMetricWithDate("my-metric-with-a-timestamp", 10, metricTimeFiveHoursAgo, false);
210+
await listener.onCompleteInvocation();
211+
212+
expect(apiScope.isDone()).toBeFalsy();
213+
});
214+
186215
it("logs metrics when logForwarding is enabled with custom timestamp", async () => {
187216
const spy = jest.spyOn(process.stdout, "write");
188217
// jest.spyOn(Date, "now").mockImplementation(() => 1487076708000);

src/metrics/listener.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { StatsD } from "hot-shots";
22
import { promisify } from "util";
3-
import { logDebug, logError } from "../utils";
3+
import { logDebug, logError, logWarning } from "../utils";
44
import { flushExtension, isExtensionRunning } from "./extension";
55
import { KMSService } from "./kms-service";
66
import { writeMetricToStdout } from "./metric-log";
77
import { Distribution } from "./model";
8+
import { Context } from "aws-lambda";
9+
import { getEnhancedMetricTags } from "./enhanced-metrics";
810

911
const METRICS_BATCH_SEND_INTERVAL = 10000; // 10 seconds
12+
const HISTORICAL_METRICS_THRESHOLD_HOURS = 4 * 60 * 60 * 1000; // 4 hours
1013

1114
export interface MetricsConfig {
1215
/**
@@ -58,13 +61,14 @@ export class MetricsListener {
5861
private apiKey: Promise<string>;
5962
private statsDClient?: StatsD;
6063
private isExtensionRunning?: boolean = undefined;
64+
private globalTags?: string[] = [];
6165

6266
constructor(private kmsClient: KMSService, private config: MetricsConfig) {
6367
this.apiKey = this.getAPIKey(config);
6468
this.config = config;
6569
}
6670

67-
public async onStartInvocation(_: any) {
71+
public async onStartInvocation(_: any, context?: Context) {
6872
if (this.isExtensionRunning === undefined) {
6973
this.isExtensionRunning = await isExtensionRunning();
7074
logDebug(`Extension present: ${this.isExtensionRunning}`);
@@ -73,6 +77,7 @@ export class MetricsListener {
7377
if (this.isExtensionRunning) {
7478
logDebug(`Using StatsD client`);
7579

80+
this.globalTags = this.getGlobalTags(context);
7681
this.statsDClient = new StatsD({ host: "127.0.0.1", closingFlushInterval: 1 });
7782
return;
7883
}
@@ -138,9 +143,18 @@ export class MetricsListener {
138143
if (this.isExtensionRunning) {
139144
const isMetricTimeValid = Date.parse(metricTime.toString()) > 0;
140145
if (isMetricTimeValid) {
146+
const dateCeiling = new Date(Date.now() - HISTORICAL_METRICS_THRESHOLD_HOURS); // 4 hours ago
147+
if (dateCeiling > metricTime) {
148+
logWarning(`Timestamp ${metricTime.toISOString()} is older than 4 hours, not submitting metric ${name}`);
149+
return;
150+
}
141151
// Only create the processor to submit metrics to the API when a user provides a valid timestamp as
142152
// Dogstatsd does not support timestamps for distributions.
143153
this.currentProcessor = this.createProcessor(this.config, this.apiKey);
154+
// Add global tags to metrics sent to the API
155+
if (this.globalTags !== undefined && this.globalTags.length > 0) {
156+
tags = [...tags, ...this.globalTags];
157+
}
144158
} else {
145159
this.statsDClient?.distribution(name, value, undefined, tags);
146160
return;
@@ -183,7 +197,7 @@ export class MetricsListener {
183197
const url = `https://api.${config.siteURL}`;
184198
const apiClient = new APIClient(key, url);
185199
const processor = new Processor(apiClient, METRICS_BATCH_SEND_INTERVAL, config.shouldRetryMetrics);
186-
processor.startProcessing();
200+
processor.startProcessing(this.globalTags);
187201
return processor;
188202
}
189203
}
@@ -202,4 +216,18 @@ export class MetricsListener {
202216
}
203217
return "";
204218
}
219+
220+
private getGlobalTags(context?: Context) {
221+
const tags = getEnhancedMetricTags(context);
222+
if (context?.invokedFunctionArn) {
223+
const splitArn = context.invokedFunctionArn.split(":");
224+
if (splitArn.length > 7) {
225+
// Get rid of the alias
226+
splitArn.pop();
227+
}
228+
const arn = splitArn.join(":");
229+
tags.push(`function_arn:${arn}`);
230+
}
231+
return tags;
232+
}
205233
}

0 commit comments

Comments
 (0)