Skip to content

Commit 530f835

Browse files
authored
Release 1.6.1 (#73)
* feat: patch bump * Esmodule support lambda (#72) * Add method to manually wrap lambda handler * Move to extension class * fix test * remove header diff across node * add testg * Handle err
1 parent d0cea57 commit 530f835

File tree

6 files changed

+220
-83
lines changed

6 files changed

+220
-83
lines changed

lambda_layer/local.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM amazon/aws-sam-cli-build-image-nodejs10.x
1+
FROM amazon/aws-sam-cli-build-image-nodejs14.x
22

33
ADD . /workspace
44

src/HypertraceAgent.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {isCompatible} from "./instrumentation/InstrumentationCompat";
33

44
require('./instrumentation/instrumentation-patch');
55

6-
import {AwsLambdaInstrumentation} from "@opentelemetry/instrumentation-aws-lambda";
6+
import {ExtendedAwsLambdaInstrumentation} from "./instrumentation/ExtendedAwsLambdaInstrumentation";
77
import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
88
import {BatchSpanProcessor, InMemorySpanExporter, SpanExporter} from '@opentelemetry/sdk-trace-base';
99
import {ZipkinExporter} from '@opentelemetry/exporter-zipkin';
@@ -59,6 +59,13 @@ export class HypertraceAgent {
5959
logger.info("Successfully initialized Hypertrace Agent")
6060
}
6161

62+
instrumentLambda(handlerFunc) {
63+
return ExtendedAwsLambdaInstrumentation.TraceLambda(handlerFunc, {
64+
requestHook: LambdaRequestHook,
65+
responseHook: LambdaResponseHook
66+
}, this._provider)
67+
}
68+
6269
instrument() {
6370
hypertraceDomain.on('error', (er) => {
6471
// these should only be forbidden errors unless something is going wrong with our body capture
@@ -85,7 +92,7 @@ export class HypertraceAgent {
8592
}
8693

8794
let instrumentations = [
88-
new AwsLambdaInstrumentation({
95+
new ExtendedAwsLambdaInstrumentation({
8996
requestHook: LambdaRequestHook,
9097
responseHook: LambdaResponseHook,
9198
disableAwsContextPropagation: true
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { AwsLambdaInstrumentation } from "@opentelemetry/instrumentation-aws-lambda";
2+
import {SemanticAttributes, SemanticResourceAttributes} from "@opentelemetry/semantic-conventions";
3+
import {SpanKind, SpanStatusCode, trace} from "@opentelemetry/api";
4+
import {
5+
context as otelContext,
6+
} from '@opentelemetry/api';
7+
8+
import {VERSION} from "@opentelemetry/instrumentation-aws-lambda/build/src/version"
9+
10+
export class ExtendedAwsLambdaInstrumentation extends AwsLambdaInstrumentation {
11+
public static TraceLambda(handler, config = {}, traceProvider) {
12+
const tracer = trace.getTracer(super.name, VERSION);
13+
14+
return async function tracedHandler(event, lambdaContext, callback) {
15+
const spanName = lambdaContext.functionName || 'Lambda Function';
16+
const span = tracer.startSpan(spanName, {
17+
kind: SpanKind.SERVER,
18+
attributes: {
19+
[SemanticAttributes.FAAS_EXECUTION]: lambdaContext.awsRequestId,
20+
[SemanticResourceAttributes.FAAS_ID]: lambdaContext.invokedFunctionArn,
21+
},
22+
});
23+
24+
// @ts-ignore
25+
if (config.requestHook) {
26+
// @ts-ignore
27+
config.requestHook(span, {event, context: lambdaContext});
28+
}
29+
30+
return otelContext.with(trace.setSpan(otelContext.active(), span), async () => {
31+
try {
32+
// Execute the original handler function
33+
const result = await handler(event, lambdaContext, callback);
34+
35+
// @ts-ignore
36+
if (config.responseHook) {
37+
// @ts-ignore
38+
config.responseHook(span, {err: null, res: result});
39+
}
40+
41+
span.end();
42+
await traceProvider.forceFlush()
43+
44+
return result;
45+
} catch (error) {
46+
// @ts-ignore
47+
if (config.responseHook) {
48+
// @ts-ignore
49+
config.responseHook(span, {err: error});
50+
}
51+
52+
span.recordException(error);
53+
span.setStatus({
54+
code: SpanStatusCode.ERROR,
55+
message: error.message,
56+
});
57+
span.end();
58+
await traceProvider.forceFlush()
59+
throw error; // Rethrow the error to ensure Lambda can handle it accordingly
60+
}
61+
});
62+
};
63+
}
64+
}

src/instrumentation/LambdaInstrumentationWrapper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export function LambdaRequestHook(span, {event, context}){
4949
}
5050

5151
export function LambdaResponseHook(span, {err, res}) : void {
52+
if(err && !res){
53+
return
54+
}
5255
let statusCode = res['statusCode']
5356
let responseHeaders = res['headers']
5457
let responseBody = res['body']

test/instrumentation/HapiTest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ if(isCompatible("12.0.0") === true){
138138
let serverSpanAttributes = spans[1].attributes
139139
expect(serverSpanAttributes['http.request.header.content-type']).to.equal('application/json')
140140
expect(serverSpanAttributes['http.request.header.host']).to.equal('localhost:8000')
141-
expect(serverSpanAttributes['http.request.header.connection']).to.equal('close')
142141

143142
expect(serverSpanAttributes['http.response.header.content-type']).to.equal('application/json; charset=utf-8')
144143
expect(serverSpanAttributes['http.response.header.x-test-header']).to.equal('some-value')

test/instrumentation/LambdaInstrumentationWrapperTest.ts

Lines changed: 143 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,95 @@
11
import {AgentForTest} from "./AgentForTest";
22
import {expect} from "chai";
33
import {LambdaRequestHook, LambdaResponseHook} from "../../src/instrumentation/LambdaInstrumentationWrapper";
4+
import {error} from "loglevel";
45
const agentTestWrapper = AgentForTest.getInstance();
56
agentTestWrapper.instrument()
67

7-
describe('Lambda test', () => {
8-
let apiGatewayEventV1 = {
9-
// "version": "1.0", version isn't always present so cant be relied on
10-
"resource": "/my/path",
11-
"path": "/my/path",
12-
"httpMethod": "PUT",
13-
"headers": {
14-
"header1": "value1",
15-
"header2": "value2",
16-
'x-forwarded-proto': 'https',
17-
'content-type': 'application/json',
18-
},
19-
"queryStringParameters": {
20-
"parameter1": "value1",
21-
"parameter2": "value"
8+
let apiGatewayEventV1 = {
9+
// "version": "1.0", version isn't always present so cant be relied on
10+
"resource": "/my/path",
11+
"path": "/my/path",
12+
"httpMethod": "PUT",
13+
"headers": {
14+
"header1": "value1",
15+
"header2": "value2",
16+
'x-forwarded-proto': 'https',
17+
'content-type': 'application/json',
18+
},
19+
"queryStringParameters": {
20+
"parameter1": "value1",
21+
"parameter2": "value"
22+
},
23+
"multiValueQueryStringParameters": {
24+
"parameter1": [
25+
"value1",
26+
"value2"
27+
],
28+
"parameter2": [
29+
"value"
30+
]
31+
},
32+
"requestContext": {
33+
"accountId": "123456789012",
34+
"apiId": "id",
35+
"authorizer": {
36+
"claims": null,
37+
"scopes": null
2238
},
23-
"multiValueQueryStringParameters": {
24-
"parameter1": [
25-
"value1",
26-
"value2"
27-
],
28-
"parameter2": [
29-
"value"
30-
]
31-
},
32-
"requestContext": {
33-
"accountId": "123456789012",
34-
"apiId": "id",
35-
"authorizer": {
36-
"claims": null,
37-
"scopes": null
38-
},
39-
"domainName": "id.execute-api.us-east-1.amazonaws.com",
40-
"domainPrefix": "id",
41-
"extendedRequestId": "request-id",
42-
"httpMethod": "GET",
43-
"identity": {
44-
"accessKey": null,
45-
"accountId": null,
46-
"caller": null,
47-
"cognitoAuthenticationProvider": null,
48-
"cognitoAuthenticationType": null,
49-
"cognitoIdentityId": null,
50-
"cognitoIdentityPoolId": null,
51-
"principalOrgId": null,
52-
"sourceIp": "192.0.2.1",
53-
"user": null,
54-
"userAgent": "user-agent",
55-
"userArn": null,
56-
"clientCert": {
57-
"clientCertPem": "CERT_CONTENT",
58-
"subjectDN": "www.example.com",
59-
"issuerDN": "Example issuer",
60-
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
61-
"validity": {
62-
"notBefore": "May 28 12:30:02 2019 GMT",
63-
"notAfter": "Aug 5 09:36:04 2021 GMT"
64-
}
39+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
40+
"domainPrefix": "id",
41+
"extendedRequestId": "request-id",
42+
"httpMethod": "GET",
43+
"identity": {
44+
"accessKey": null,
45+
"accountId": null,
46+
"caller": null,
47+
"cognitoAuthenticationProvider": null,
48+
"cognitoAuthenticationType": null,
49+
"cognitoIdentityId": null,
50+
"cognitoIdentityPoolId": null,
51+
"principalOrgId": null,
52+
"sourceIp": "192.0.2.1",
53+
"user": null,
54+
"userAgent": "user-agent",
55+
"userArn": null,
56+
"clientCert": {
57+
"clientCertPem": "CERT_CONTENT",
58+
"subjectDN": "www.example.com",
59+
"issuerDN": "Example issuer",
60+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
61+
"validity": {
62+
"notBefore": "May 28 12:30:02 2019 GMT",
63+
"notAfter": "Aug 5 09:36:04 2021 GMT"
6564
}
66-
},
67-
"path": "/my/path",
68-
"protocol": "HTTP/1.1",
69-
"requestId": "id=",
70-
"requestTime": "04/Mar/2020:19:15:17 +0000",
71-
"requestTimeEpoch": 1583349317135,
72-
"resourceId": null,
73-
"resourcePath": "/my/path",
74-
"stage": "$default"
65+
}
7566
},
76-
"pathParameters": null,
77-
"stageVariables": null,
78-
"body": '{\n\t"req-body": "some-data"\n}',
79-
"isBase64Encoded": false
80-
}
67+
"path": "/my/path",
68+
"protocol": "HTTP/1.1",
69+
"requestId": "id=",
70+
"requestTime": "04/Mar/2020:19:15:17 +0000",
71+
"requestTimeEpoch": 1583349317135,
72+
"resourceId": null,
73+
"resourcePath": "/my/path",
74+
"stage": "$default"
75+
},
76+
"pathParameters": null,
77+
"stageVariables": null,
78+
"body": '{\n\t"req-body": "some-data"\n}',
79+
"isBase64Encoded": false
80+
}
81+
82+
let response = {
83+
"statusCode": "200",
84+
"headers": {
85+
"a-Header": "some_VALUE",
86+
"Content-Type": "application/json"
87+
},
88+
"body": JSON.stringify({"some_body_data": "response-data"})
89+
}
90+
91+
92+
describe('Lambda test', () => {
8193

8294
let apiGatewayEventV2 = {
8395
// "version": "2.0", version isn't always present so cant be relied on
@@ -114,14 +126,6 @@ describe('Lambda test', () => {
114126
body: '{\n\t"req-body": "some-data"\n}',
115127
isBase64Encoded: false
116128
}
117-
let response = {
118-
"statusCode": "200",
119-
"headers": {
120-
"a-Header": "some_VALUE",
121-
"Content-Type": "application/json"
122-
},
123-
"body": JSON.stringify({"some_body_data": "response-data"})
124-
}
125129

126130
beforeEach(() => {
127131
agentTestWrapper.stop()
@@ -200,3 +204,63 @@ describe('Lambda test', () => {
200204
expect(lambdaSpan.attributes['http.response.body']).to.equal('{"some_body_data":"response-data"}')
201205
})
202206
})
207+
208+
describe("manually instrument lambda function", () => {
209+
beforeEach(() => {
210+
agentTestWrapper.stop()
211+
})
212+
213+
afterEach( ()=> {
214+
agentTestWrapper.stop()
215+
})
216+
it('can be manually instrumented', async () => {
217+
async function myHandler(event, context, callback){
218+
return response
219+
}
220+
221+
let wrappedHandler = agentTestWrapper.instrumentLambda(myHandler)
222+
await wrappedHandler(apiGatewayEventV1, {}, () => {})
223+
224+
let spans = agentTestWrapper.getSpans()
225+
expect(spans.length).to.equal(1)
226+
let lambdaSpan = spans[0]
227+
expect(lambdaSpan.attributes['http.method']).to.equal('PUT')
228+
expect(lambdaSpan.attributes['http.scheme']).to.equal('https')
229+
expect(lambdaSpan.attributes['http.host']).to.equal('id.execute-api.us-east-1.amazonaws.com')
230+
expect(lambdaSpan.attributes['http.target']).to.equal('/my/path')
231+
expect(lambdaSpan.attributes['http.request.header.content-type']).to.equal('application/json')
232+
expect(lambdaSpan.attributes['http.request.header.header1']).to.equal('value1')
233+
expect(lambdaSpan.attributes['http.request.body']).to.equal('{\n' +
234+
'\t"req-body": "some-data"\n' +
235+
'}')
236+
expect(lambdaSpan.attributes['http.status_code']).to.equal('200')
237+
expect(lambdaSpan.attributes['http.response.header.a-header']).to.equal('some_VALUE')
238+
expect(lambdaSpan.attributes['http.response.header.content-type']).to.equal('application/json')
239+
expect(lambdaSpan.attributes['http.response.body']).to.equal('{"some_body_data":"response-data"}')
240+
})
241+
242+
it('can be manually instrumented and handle error', async () => {
243+
async function myHandler(event, context, callback){
244+
throw new Error("some error")
245+
}
246+
247+
let wrappedHandler = agentTestWrapper.instrumentLambda(myHandler)
248+
try {
249+
await wrappedHandler(apiGatewayEventV1, {}, () => {})
250+
} catch(_){}
251+
252+
253+
let spans = agentTestWrapper.getSpans()
254+
expect(spans.length).to.equal(1)
255+
let lambdaSpan = spans[0]
256+
expect(lambdaSpan.attributes['http.method']).to.equal('PUT')
257+
expect(lambdaSpan.attributes['http.scheme']).to.equal('https')
258+
expect(lambdaSpan.attributes['http.host']).to.equal('id.execute-api.us-east-1.amazonaws.com')
259+
expect(lambdaSpan.attributes['http.target']).to.equal('/my/path')
260+
expect(lambdaSpan.attributes['http.request.header.content-type']).to.equal('application/json')
261+
expect(lambdaSpan.attributes['http.request.header.header1']).to.equal('value1')
262+
expect(lambdaSpan.attributes['http.request.body']).to.equal('{\n' +
263+
'\t"req-body": "some-data"\n' +
264+
'}')
265+
})
266+
})

0 commit comments

Comments
 (0)