Skip to content

Commit 5415f51

Browse files
astuyvechris.agocshghotra
authored
Inferred spans for Lambda function URLs (#247)
* Savepoint. Add trigger and event type logic. Start working out how to infer the span correctly * feat: Create and finish inferred span. Handle parenting. Ensure completed span on invocation finish * feat: Fix tests and guard clauses * tests: Trigger specs added and working * feat: Fix http method on createInferredSpanForLambdaUrl. Add test for SpanInferrer * feat: Rename inferred-spans to span-inferrer, as it exports a class & follows command pattern. Update imports * feat: Use the magical resource.name and span.type to correctly set the attributes for JS * Switch to truthy case statement Co-authored-by: Harvinder Ghotra <[email protected]> Co-authored-by: chris.agocs <[email protected]> Co-authored-by: Harvinder Ghotra <[email protected]>
1 parent d2e40f0 commit 5415f51

10 files changed

+241
-22
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "$default",
4+
"rawPath": "/",
5+
"rawQueryString": "",
6+
"headers": {
7+
"sec-fetch-mode": "navigate",
8+
"sec-fetch-site": "none",
9+
"accept-language": "en-US,en;q=0.9",
10+
"x-forwarded-proto": "https",
11+
"x-forwarded-port": "443",
12+
"x-forwarded-for": "71.195.30.42",
13+
"sec-fetch-user": "?1",
14+
"pragma": "no-cache",
15+
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
16+
"sec-ch-ua": "\"Google Chrome\";v=\"95\", \"Chromium\";v=\"95\", \";Not A Brand\";v=\"99\"",
17+
"sec-ch-ua-mobile": "?0",
18+
"x-amzn-trace-id": "Root=1-61953929-1ec00c3011062a48477b169e",
19+
"sec-ch-ua-platform": "\"macOS\"",
20+
"host": "a8hyhsshac.lambda-url.eu-south-1.amazonaws.com",
21+
"upgrade-insecure-requests": "1",
22+
"cache-control": "no-cache",
23+
"accept-encoding": "gzip, deflate, br",
24+
"sec-fetch-dest": "document",
25+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
26+
},
27+
"requestContext": {
28+
"accountId": "601427279990",
29+
"apiId": "a8hyhsshac",
30+
"domainName": "a8hyhsshac.lambda-url.eu-south-1.amazonaws.com",
31+
"domainPrefix": "a8hyhsshac",
32+
"http": {
33+
"method": "GET",
34+
"path": "/",
35+
"protocol": "HTTP/1.1",
36+
"sourceIp": "71.195.30.42",
37+
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
38+
},
39+
"requestId": "ec4d58f8-2b8b-4ceb-a1d5-2be7bff58505",
40+
"routeKey": "$default",
41+
"stage": "$default",
42+
"time": "17/Nov/2021:17:17:29 +0000",
43+
"timeEpoch": 1637169449721
44+
},
45+
"isBase64Encoded": false
46+
}

src/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ describe("datadog", () => {
331331

332332
expect(mockedIncrementInvocations).toBeCalledTimes(1);
333333
expect(mockedIncrementInvocations).toBeCalledWith(expect.anything(), mockContext);
334-
expect(logger.debug).toHaveBeenCalledTimes(9);
334+
expect(logger.debug).toHaveBeenCalledTimes(10);
335335
expect(logger.debug).toHaveBeenLastCalledWith('{"status":"debug","message":"datadog:Unpatching HTTP libraries"}');
336336
});
337337
});

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ export function datadog<TEvent, TResult>(
150150
if (traceListener.currentSpan) {
151151
traceListener.currentSpan.setTag("http.status_code", statusCode);
152152
}
153+
if (traceListener.currentInferredSpan) {
154+
traceListener.currentInferredSpan.setTag("http.status_code", statusCode);
155+
}
153156
}
154157
}
155158
}

src/trace/listener.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { datadogLambdaVersion } from "../constants";
1717
import { Source, ddtraceVersion } from "./constants";
1818
import { patchConsole } from "./patch-console";
1919
import { SpanContext, TraceOptions, TracerWrapper } from "./tracer-wrapper";
20+
import { SpanInferrer } from "./span-inferrer";
2021

2122
export type TraceExtractor = (event: any, context: Context) => TraceContext;
2223

@@ -50,6 +51,8 @@ export class TraceListener {
5051
private context?: Context;
5152
private stepFunctionContext?: StepFunctionContext;
5253
private tracerWrapper: TracerWrapper;
54+
private inferrer: SpanInferrer;
55+
private inferredSpan: any;
5356

5457
public triggerTags?: { [key: string]: string };
5558
public get currentTraceHeaders() {
@@ -58,9 +61,13 @@ export class TraceListener {
5861
public get currentSpan() {
5962
return this.tracerWrapper.currentSpan;
6063
}
64+
public get currentInferredSpan() {
65+
return this.inferredSpan;
66+
}
6167
constructor(private config: TraceConfig, private handlerName: string) {
6268
this.tracerWrapper = new TracerWrapper();
6369
this.contextService = new TraceContextService(this.tracerWrapper);
70+
this.inferrer = new SpanInferrer(this.tracerWrapper);
6471
}
6572

6673
public onStartInvocation(event: any, context: Context) {
@@ -79,7 +86,8 @@ export class TraceListener {
7986
} else {
8087
logDebug("Not patching HTTP libraries", { autoPatchHTTP: this.config.autoPatchHTTP, tracerInitialized });
8188
}
82-
89+
logDebug("Creating inferred span");
90+
this.inferredSpan = this.inferrer.createInferredSpan(event, context);
8391
this.context = context;
8492
this.triggerTags = extractTriggerTags(event, context);
8593
this.contextService.rootTraceContext = extractTraceContext(event, context, this.config.traceExtractor);
@@ -98,6 +106,10 @@ export class TraceListener {
98106
logDebug("Unpatching HTTP libraries");
99107
unpatchHttp();
100108
}
109+
if (this.inferredSpan) {
110+
logDebug("Finishing inferred span");
111+
this.inferredSpan.finish(Date.now());
112+
}
101113
}
102114

103115
public onWrap<T = (...args: any[]) => any>(func: T): T {
@@ -147,8 +159,12 @@ export class TraceListener {
147159
...this.stepFunctionContext,
148160
};
149161
}
150-
151-
if (parentSpanContext !== null) {
162+
if (this.inferredSpan) {
163+
options.childOf = this.inferredSpan;
164+
if (parentSpanContext !== null) {
165+
this.inferredSpan.childOf = parentSpanContext;
166+
}
167+
} else if (parentSpanContext !== null) {
152168
options.childOf = parentSpanContext;
153169
}
154170
options.type = "serverless";

src/trace/span-inferrer.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { SpanInferrer } from "./span-inferrer";
2+
import { TracerWrapper } from "./tracer-wrapper";
3+
const lambdaURLEvent = require("../../event_samples/lambda-function-urls.json");
4+
5+
const mockWrapper = {
6+
startSpan: jest.fn(),
7+
};
8+
9+
describe("SpanInferrer", () => {
10+
it("creates an inferred span for lambda function URLs", () => {
11+
const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper);
12+
inferrer.createInferredSpan(lambdaURLEvent, {
13+
awsRequestId: "abcd-1234",
14+
} as any);
15+
16+
expect(mockWrapper.startSpan).toBeCalledWith("aws.lambda.url", {
17+
service: "aws.lambda",
18+
startTime: 1637169449721,
19+
tags: {
20+
endpoint: "/",
21+
"http.method": "GET",
22+
"http.url": "a8hyhsshac.lambda-url.eu-south-1.amazonaws.com/",
23+
operation_name: "aws.lambda.url",
24+
request_id: "abcd-1234",
25+
"resource.name": "a8hyhsshac.lambda-url.eu-south-1.amazonaws.com/",
26+
resource_names: "a8hyhsshac.lambda-url.eu-south-1.amazonaws.com/",
27+
"span.type": "http",
28+
},
29+
});
30+
});
31+
});

src/trace/span-inferrer.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Context } from "aws-lambda";
2+
import { SpanOptions, TracerWrapper } from "./tracer-wrapper";
3+
import { eventSources, parseEventSource } from "./trigger";
4+
5+
export class SpanInferrer {
6+
traceWrapper: TracerWrapper;
7+
constructor(traceWrapper: TracerWrapper) {
8+
this.traceWrapper = traceWrapper;
9+
}
10+
11+
public createInferredSpan(event: any, context: Context | undefined): any {
12+
const eventSource = parseEventSource(event);
13+
if (eventSource === eventSources.lambdaUrl) {
14+
return this.createInferredSpanForLambdaUrl(event, context);
15+
}
16+
}
17+
18+
createInferredSpanForLambdaUrl(event: any, context: Context | undefined): any {
19+
const options: SpanOptions = {};
20+
const domain = event.requestContext.domainName;
21+
const path = event.rawPath;
22+
options.tags = {
23+
operation_name: "aws.lambda.url",
24+
"http.url": domain + path,
25+
endpoint: path,
26+
"http.method": event.requestContext.http.method,
27+
resource_names: domain + path,
28+
request_id: context?.awsRequestId,
29+
"span.type": "http",
30+
"resource.name": domain + path,
31+
};
32+
options.service = "aws.lambda";
33+
options.startTime = event.requestContext.timeEpoch;
34+
35+
return this.traceWrapper.startSpan("aws.lambda.url", options);
36+
}
37+
}

src/trace/tracer-wrapper.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export interface TraceOptions {
1616
childOf?: SpanContext;
1717
}
1818

19+
export interface SpanOptions {
20+
childOf?: SpanContext;
21+
tags?: { [key: string]: any };
22+
startTime?: number;
23+
service?: string;
24+
type?: string;
25+
}
26+
1927
// TraceWrapper is used to remove dd-trace as a hard dependency from the npm package.
2028
// This lets a customer bring their own version of the tracer.
2129
export class TracerWrapper {
@@ -61,6 +69,13 @@ export class TracerWrapper {
6169
return this.tracer.wrap(name, options, fn);
6270
}
6371

72+
public startSpan<T = (...args: any[]) => any>(name: string, options: TraceOptions): T | null {
73+
if (!this.isTracerAvailable) {
74+
return null;
75+
}
76+
return this.tracer.startSpan(name, options);
77+
}
78+
6479
public traceContext(): TraceContext | undefined {
6580
if (!this.isTracerAvailable) {
6681
return;

src/trace/trigger.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ describe("parseEventSource", () => {
3030
},
3131
file: "api-gateway-v2.json",
3232
},
33+
{
34+
result: {
35+
"function_trigger.event_source": "lambda-function-url",
36+
"http.url": "a8hyhsshac.lambda-url.eu-south-1.amazonaws.com",
37+
"http.url_details.path": "/",
38+
"http.method": "GET",
39+
},
40+
file: "lambda-function-urls.json",
41+
},
3342
{
3443
result: {
3544
"function_trigger.event_source": "application-load-balancer",
@@ -146,7 +155,14 @@ describe("parseEventSource", () => {
146155
for (let response of responses) {
147156
const statusCode = extractHTTPStatusCodeTag(triggerTags, response.responseBody);
148157
// We should always return a status code for API Gateway and ALB
149-
if (["api-gateway-v1.json", "api-gateway-v2.json", "application-load-balancer.json"].includes(event.file)) {
158+
if (
159+
[
160+
"api-gateway-v1.json",
161+
"api-gateway-v2.json",
162+
"application-load-balancer.json",
163+
"lambda-function-urls.json",
164+
].includes(event.file)
165+
) {
150166
expect(statusCode).toEqual(response.expectedStatusCode);
151167
} else {
152168
expect(statusCode).toBeUndefined();

0 commit comments

Comments
 (0)