Skip to content

Commit 5b2f1bb

Browse files
authored
Merge pull request #28 from DataDog/stephenf/add-enhanced-metrics
Add enhanced lambda metrics
2 parents 2e6ee43 + edd7466 commit 5b2f1bb

15 files changed

+270
-16
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ How much logging datadog-lambda-layer-js should do. Set this to "debug" for exte
7979

8080
If you have the Datadog Lambda Log forwarder enabled and are sending custom metrics, set this to true so your metrics will be sent via logs, (instead of being sent at the end of your lambda invocation).
8181

82+
### DD_ENHANCED_METRICS
83+
84+
If you set the value of this variable to "true" then the Lambda layer will increment a Lambda integration metric called `aws.lambda.enhanced.invocations` with each invocation and `aws.lambda.enhanced.errors` if the invocation results in an error. These metrics are tagged with the function name, region, and account, as well as `cold_start:true|false`.
85+
8286
## Usage
8387

8488
Datadog needs to be able to read headers from the incoming Lambda event.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "datadog-lambda-js",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "Lambda client library that supports hybrid tracing in node js",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/index.spec.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import http from "http";
22
import nock from "nock";
33

4+
import { Context, Handler } from "aws-lambda";
45
import { datadog, getTraceHeaders, sendDistributionMetric, TraceHeaders } from "./index";
6+
import { incrementErrorsMetric, incrementInvocationsMetric } from "./metrics/enhanced-metrics";
7+
import { MetricsListener } from "./metrics/listener";
58
import { LogLevel, setLogLevel } from "./utils";
6-
import tracer, { Span } from "dd-trace";
9+
10+
jest.mock("./metrics/enhanced-metrics");
11+
12+
const mockedIncrementErrors = incrementErrorsMetric as jest.Mock<typeof incrementErrorsMetric>;
13+
const mockedIncrementInvocations = incrementInvocationsMetric as jest.Mock<typeof incrementInvocationsMetric>;
14+
15+
const mockARN = "arn:aws:lambda:us-east-1:123497598159:function:my-test-lambda";
16+
const mockContext = ({
17+
invokedFunctionArn: mockARN,
18+
} as any) as Context;
19+
20+
// const MockedListener = OriginalListenerModule.MetricsListener as jest.Mocked<
21+
// typeof OriginalListenerModule.MetricsListener
22+
// >;
723

824
describe("datadog", () => {
925
let traceId: string | undefined;
@@ -27,6 +43,9 @@ describe("datadog", () => {
2743
oldEnv = process.env;
2844
process.env = { ...oldEnv };
2945
nock.cleanAll();
46+
47+
mockedIncrementErrors.mockClear();
48+
mockedIncrementInvocations.mockClear();
3049
});
3150
afterEach(() => {
3251
process.env = oldEnv;
@@ -162,4 +181,70 @@ describe("datadog", () => {
162181
"x-datadog-trace-id": "123456",
163182
});
164183
});
184+
185+
it("increments invocations for each function call with env var", async () => {
186+
process.env.DD_ENHANCED_METRICS = "true";
187+
const wrapped = datadog(handler);
188+
189+
await wrapped({}, mockContext, () => {});
190+
191+
expect(mockedIncrementInvocations).toBeCalledTimes(1);
192+
expect(mockedIncrementInvocations).toBeCalledWith(mockARN);
193+
194+
await wrapped({}, mockContext, () => {});
195+
await wrapped({}, mockContext, () => {});
196+
await wrapped({}, mockContext, () => {});
197+
198+
expect(mockedIncrementInvocations).toBeCalledTimes(4);
199+
});
200+
201+
it("increments errors correctly with env var", async () => {
202+
process.env.DD_ENHANCED_METRICS = "true";
203+
204+
const handlerError: Handler = (event, context, callback) => {
205+
throw Error("Some error");
206+
};
207+
208+
const wrappedHandler = datadog(handlerError);
209+
210+
const result = wrappedHandler({}, mockContext, () => {});
211+
await expect(result).rejects.toEqual(Error("Some error"));
212+
213+
expect(mockedIncrementInvocations).toBeCalledTimes(1);
214+
expect(mockedIncrementErrors).toBeCalledTimes(1);
215+
216+
expect(mockedIncrementInvocations).toBeCalledWith(mockARN);
217+
expect(mockedIncrementErrors).toBeCalledWith(mockARN);
218+
});
219+
220+
it("increments errors and invocations with config setting", async () => {
221+
const handlerError: Handler = (event, context, callback) => {
222+
throw Error("Some error");
223+
};
224+
225+
const wrappedHandler = datadog(handlerError, { enhancedMetrics: true });
226+
227+
const result = wrappedHandler({}, mockContext, () => {});
228+
await expect(result).rejects.toEqual(Error("Some error"));
229+
230+
expect(mockedIncrementInvocations).toBeCalledTimes(1);
231+
expect(mockedIncrementErrors).toBeCalledTimes(1);
232+
233+
expect(mockedIncrementInvocations).toBeCalledWith(mockARN);
234+
expect(mockedIncrementErrors).toBeCalledWith(mockARN);
235+
});
236+
237+
it("doesn't increment enhanced metrics without env var or config", async () => {
238+
const handlerError: Handler = (event, context, callback) => {
239+
throw Error("Some error");
240+
};
241+
242+
const wrappedHandler = datadog(handlerError);
243+
244+
const result = wrappedHandler({}, mockContext, () => {});
245+
await expect(result).rejects.toEqual(Error("Some error"));
246+
247+
expect(mockedIncrementInvocations).toBeCalledTimes(0);
248+
expect(mockedIncrementErrors).toBeCalledTimes(0);
249+
});
165250
});

src/index.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Handler } from "aws-lambda";
22

3-
import { KMSService, MetricsConfig, MetricsListener } from "./metrics";
3+
import {
4+
incrementErrorsMetric,
5+
incrementInvocationsMetric,
6+
KMSService,
7+
MetricsConfig,
8+
MetricsListener,
9+
} from "./metrics";
410
import { TraceConfig, TraceHeaders, TraceListener } from "./trace";
511
import { logError, LogLevel, setLogLevel, wrap } from "./utils";
612

@@ -11,6 +17,7 @@ const apiKeyKMSEnvVar = "DD_KMS_API_KEY";
1117
const siteURLEnvVar = "DD_SITE";
1218
const logLevelEnvVar = "DD_LOG_LEVEL";
1319
const logForwardingEnvVar = "DD_FLUSH_TO_LOG";
20+
const enhancedMetricsEnvVar = "DD_ENHANCED_METRICS";
1421

1522
const defaultSiteURL = "datadoghq.com";
1623

@@ -32,6 +39,7 @@ export const defaultConfig: Config = {
3239
apiKeyKMS: "",
3340
autoPatchHTTP: true,
3441
debugLogging: false,
42+
enhancedMetrics: false,
3543
logForwarding: false,
3644
shouldRetryMetrics: false,
3745
siteURL: "",
@@ -72,8 +80,14 @@ export function datadog<TEvent, TResult>(
7280
for (const listener of listeners) {
7381
listener.onStartInvocation(event, context);
7482
}
83+
if (finalConfig.enhancedMetrics) {
84+
incrementInvocationsMetric(context.invokedFunctionArn);
85+
}
7586
},
76-
async () => {
87+
async (event, context, error?) => {
88+
if (finalConfig.enhancedMetrics && error) {
89+
incrementErrorsMetric(context.invokedFunctionArn);
90+
}
7791
// Completion hook, (called once per handler invocation)
7892
for (const listener of listeners) {
7993
await listener.onCompleteInvocation();
@@ -142,11 +156,15 @@ function getConfig(userConfig?: Partial<Config>): Config {
142156
const result = getEnvValue(logForwardingEnvVar, "false").toLowerCase();
143157
config.logForwarding = result === "true";
144158
}
159+
if (userConfig === undefined || userConfig.enhancedMetrics === undefined) {
160+
const result = getEnvValue(enhancedMetricsEnvVar, "false").toLowerCase();
161+
config.enhancedMetrics = result === "true";
162+
}
145163

146164
return config;
147165
}
148166

149-
function getEnvValue(key: string, defaultValue: string): string {
167+
export function getEnvValue(key: string, defaultValue: string): string {
150168
const val = process.env[key];
151169
return val !== undefined ? val : defaultValue;
152170
}

src/metrics/enhanced-metrics.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { getEnvValue, sendDistributionMetric } from "../index";
2+
3+
import { parseTagsFromARN } from "../utils/arn";
4+
import { getColdStartTag } from "../utils/cold-start";
5+
6+
const ENHANCED_LAMBDA_METRICS_NAMESPACE = "aws.lambda.enhanced";
7+
8+
export function incrementInvocationsMetric(functionARN: string): void {
9+
const tags = [...parseTagsFromARN(functionARN), getColdStartTag()];
10+
sendDistributionMetric(`${ENHANCED_LAMBDA_METRICS_NAMESPACE}.invocations`, 1, ...tags);
11+
}
12+
13+
export function incrementErrorsMetric(functionARN: string): void {
14+
const tags = [...parseTagsFromARN(functionARN), getColdStartTag()];
15+
sendDistributionMetric(`${ENHANCED_LAMBDA_METRICS_NAMESPACE}.errors`, 1, ...tags);
16+
}

src/metrics/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { MetricsConfig, MetricsListener } from "./listener";
22
export { KMSService } from "./kms-service";
3+
export { incrementErrorsMetric, incrementInvocationsMetric } from "./enhanced-metrics";

src/metrics/listener.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe("MetricsListener", () => {
2626
const listener = new MetricsListener(kms as any, {
2727
apiKey: "api-key",
2828
apiKeyKMS: "kms-api-key-encrypted",
29+
enhancedMetrics: false,
2930
logForwarding: false,
3031
shouldRetryMetrics: false,
3132
siteURL,
@@ -46,6 +47,7 @@ describe("MetricsListener", () => {
4647
const listener = new MetricsListener(kms as any, {
4748
apiKey: "",
4849
apiKeyKMS: "kms-api-key-encrypted",
50+
enhancedMetrics: false,
4951
logForwarding: false,
5052
shouldRetryMetrics: false,
5153
siteURL,
@@ -62,6 +64,7 @@ describe("MetricsListener", () => {
6264
const listener = new MetricsListener(kms as any, {
6365
apiKey: "",
6466
apiKeyKMS: "kms-api-key-encrypted",
67+
enhancedMetrics: false,
6568
logForwarding: false,
6669
shouldRetryMetrics: false,
6770
siteURL,

src/metrics/listener.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export interface MetricsConfig {
3838
* @default false
3939
*/
4040
logForwarding: boolean;
41+
42+
/**
43+
* Whether to increment invocations and errors Lambda integration metrics from this layer.
44+
* @default false
45+
*/
46+
enhancedMetrics: boolean;
4147
}
4248

4349
export class MetricsListener {

src/trace/listener.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { extractTraceContext } from "./context";
55
import { patchHttp, unpatchHttp } from "./patch-http";
66
import { TraceContextService } from "./trace-context-service";
77

8+
import { didFunctionColdStart } from "../utils/cold-start";
9+
810
export interface TraceConfig {
911
/**
1012
* Whether to automatically patch all outgoing http requests with Datadog's hybrid tracing headers.
@@ -16,7 +18,6 @@ export interface TraceConfig {
1618
export class TraceListener {
1719
private contextService = new TraceContextService();
1820
private context?: Context;
19-
private coldstart = true;
2021

2122
public get currentTraceHeaders() {
2223
return this.contextService.currentTraceHeaders;
@@ -37,7 +38,6 @@ export class TraceListener {
3738
if (this.config.autoPatchHTTP) {
3839
unpatchHttp();
3940
}
40-
this.coldstart = false;
4141
}
4242

4343
public onWrap<T = (...args: any[]) => any>(func: T): T {
@@ -46,7 +46,7 @@ export class TraceListener {
4646
const options: SpanOptions & TraceOptions = {};
4747
if (this.context) {
4848
options.tags = {
49-
cold_start: this.coldstart,
49+
cold_start: didFunctionColdStart(),
5050
function_arn: this.context.invokedFunctionArn,
5151
request_id: this.context.awsRequestId,
5252
resource_names: this.context.functionName,

src/utils/arn.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { parseLambdaARN, parseTagsFromARN } from "./arn";
2+
3+
describe("arn utils", () => {
4+
it("parses arn properties", () => {
5+
expect(parseLambdaARN("arn:aws:lambda:us-east-1:123497598159:function:my-test-lambda")).toEqual({
6+
account_id: "123497598159",
7+
functionname: "my-test-lambda",
8+
region: "us-east-1",
9+
});
10+
});
11+
12+
it("parses arn properties with version alias", () => {
13+
expect(parseLambdaARN("arn:aws:lambda:us-east-1:123497598159:function:my-test-lambda:my-version-alias")).toEqual({
14+
account_id: "123497598159",
15+
functionname: "my-test-lambda",
16+
region: "us-east-1",
17+
});
18+
});
19+
20+
it("parses arn tags", () => {
21+
const parsedTags = parseTagsFromARN("arn:aws:lambda:us-east-1:123497598159:function:my-test-lambda");
22+
for (const tag of ["account_id:123497598159", "functionname:my-test-lambda", "region:us-east-1"]) {
23+
expect(parsedTags).toContain(tag);
24+
}
25+
});
26+
27+
it("parses arn tags with version", () => {
28+
const parsedTags = parseTagsFromARN(
29+
"arn:aws:lambda:us-east-1:123497598159:function:my-test-lambda:my-version-alias",
30+
);
31+
for (const tag of ["account_id:123497598159", "functionname:my-test-lambda", "region:us-east-1"]) {
32+
expect(parsedTags).toContain(tag);
33+
}
34+
});
35+
});

0 commit comments

Comments
 (0)