3
3
// - Added Sentry `wrapHandler` around the OTel patch handler.
4
4
// - Cancel init when handler string is invalid (TS)
5
5
// - Hardcoded package version and name
6
+ // - Added support for streaming handlers
6
7
/* eslint-disable */
7
8
/*
8
9
* Copyright The OpenTelemetry Authors
@@ -50,7 +51,7 @@ import {
50
51
SEMRESATTRS_CLOUD_ACCOUNT_ID ,
51
52
SEMRESATTRS_FAAS_ID ,
52
53
} from '@opentelemetry/semantic-conventions' ;
53
- import type { APIGatewayProxyEventHeaders , Callback , Context , Handler } from 'aws-lambda' ;
54
+ import type { APIGatewayProxyEventHeaders , Callback , Context , Handler , StreamifyHandler } from 'aws-lambda' ;
54
55
import * as fs from 'fs' ;
55
56
import * as path from 'path' ;
56
57
import type { LambdaModule } from './internal-types' ;
@@ -73,6 +74,9 @@ const headerGetter: TextMapGetter<APIGatewayProxyEventHeaders> = {
73
74
} ;
74
75
75
76
export const lambdaMaxInitInMilliseconds = 10_000 ;
77
+ const AWS_HANDLER_STREAMING_SYMBOL = Symbol . for ( 'aws.lambda.runtime.handler.streaming' ) ;
78
+ const AWS_HANDLER_HIGHWATERMARK_SYMBOL = Symbol . for ( 'aws.lambda.runtime.handler.streaming.highWaterMark' ) ;
79
+ const AWS_HANDLER_STREAMING_RESPONSE = 'response' ;
76
80
77
81
/**
78
82
*
@@ -101,6 +105,21 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
101
105
return [ ] ;
102
106
}
103
107
108
+ // Provide a temporary awslambda polyfill for CommonJS modules during loading
109
+ // This prevents ReferenceError when modules use awslambda.streamifyResponse at load time
110
+ // taken from https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/main/src/UserFunction.js#L205C7-L211C9
111
+ if ( typeof globalThis . awslambda === 'undefined' ) {
112
+ ( globalThis as any ) . awslambda = {
113
+ streamifyResponse : ( handler : any , options : any ) => {
114
+ handler [ AWS_HANDLER_STREAMING_SYMBOL ] = AWS_HANDLER_STREAMING_RESPONSE ;
115
+ if ( typeof options ?. highWaterMark === 'number' ) {
116
+ handler [ AWS_HANDLER_HIGHWATERMARK_SYMBOL ] = parseInt ( options . highWaterMark ) ;
117
+ }
118
+ return handler ;
119
+ } ,
120
+ } ;
121
+ }
122
+
104
123
const handler = path . basename ( handlerDef ) ;
105
124
const moduleRoot = handlerDef . substring ( 0 , handlerDef . length - handler . length ) ;
106
125
@@ -187,16 +206,33 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
187
206
/**
188
207
*
189
208
*/
190
- private _getHandler ( handlerLoadStartTime : number ) {
191
- return ( original : Handler ) => {
192
- return wrapHandler ( this . _getPatchHandler ( original , handlerLoadStartTime ) ) ;
209
+ private _getHandler < T extends Handler | StreamifyHandler > ( handlerLoadStartTime : number ) {
210
+ return ( original : T ) : T => {
211
+ if ( this . _isStreamingHandler ( original ) ) {
212
+ const patchedHandler = this . _getPatchHandler ( original , handlerLoadStartTime ) ;
213
+
214
+ // Streaming handlers have special symbols that we need to copy over to the patched handler.
215
+ ( patchedHandler as unknown as Record < symbol , unknown > ) [ AWS_HANDLER_STREAMING_SYMBOL ] = (
216
+ original as unknown as Record < symbol , unknown >
217
+ ) [ AWS_HANDLER_STREAMING_SYMBOL ] ;
218
+ ( patchedHandler as unknown as Record < symbol , unknown > ) [ AWS_HANDLER_HIGHWATERMARK_SYMBOL ] = (
219
+ original as unknown as Record < symbol , unknown >
220
+ ) [ AWS_HANDLER_HIGHWATERMARK_SYMBOL ] ;
221
+
222
+ return wrapHandler ( patchedHandler ) as T ;
223
+ }
224
+
225
+ return wrapHandler ( this . _getPatchHandler ( original , handlerLoadStartTime ) ) as T ;
193
226
} ;
194
227
}
195
228
229
+ private _getPatchHandler ( original : Handler , lambdaStartTime : number ) : Handler ;
230
+ private _getPatchHandler ( original : StreamifyHandler , lambdaStartTime : number ) : StreamifyHandler ;
231
+
196
232
/**
197
233
*
198
234
*/
199
- private _getPatchHandler ( original : Handler , lambdaStartTime : number ) {
235
+ private _getPatchHandler ( original : Handler | StreamifyHandler , lambdaStartTime : number ) : Handler | StreamifyHandler {
200
236
diag . debug ( 'patch handler function' ) ;
201
237
const plugin = this ;
202
238
@@ -229,6 +265,36 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
229
265
}
230
266
}
231
267
268
+ if ( this . _isStreamingHandler ( original ) ) {
269
+ return function patchedStreamingHandler (
270
+ this : never ,
271
+ // The event can be a user type, it truly is any.
272
+ event : any ,
273
+ responseStream : Parameters < StreamifyHandler > [ 1 ] ,
274
+ context : Context ,
275
+ ) {
276
+ _onRequest ( ) ;
277
+ const parent = plugin . _determineParent ( event , context ) ;
278
+ const span = plugin . _createSpanForRequest ( event , context , requestIsColdStart , parent ) ;
279
+ plugin . _applyRequestHook ( span , event , context ) ;
280
+
281
+ return otelContext . with ( trace . setSpan ( parent , span ) , ( ) => {
282
+ const maybePromise = safeExecuteInTheMiddle (
283
+ ( ) => original . apply ( this , [ event , responseStream , context ] ) ,
284
+ error => {
285
+ if ( error != null ) {
286
+ // Exception thrown synchronously before resolving promise.
287
+ plugin . _applyResponseHook ( span , error ) ;
288
+ plugin . _endSpan ( span , error , ( ) => { } ) ;
289
+ }
290
+ } ,
291
+ ) as Promise < { } > | undefined ;
292
+
293
+ return plugin . _handlePromiseResult ( span , maybePromise ) ;
294
+ } ) ;
295
+ } ;
296
+ }
297
+
232
298
return function patchedHandler (
233
299
this : never ,
234
300
// The event can be a user type, it truly is any.
@@ -239,39 +305,10 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
239
305
) {
240
306
_onRequest ( ) ;
241
307
242
- const config = plugin . getConfig ( ) ;
243
- const parent = AwsLambdaInstrumentation . _determineParent (
244
- event ,
245
- context ,
246
- config . eventContextExtractor || AwsLambdaInstrumentation . _defaultEventContextExtractor ,
247
- ) ;
308
+ const parent = plugin . _determineParent ( event , context ) ;
248
309
249
- const name = context . functionName ;
250
- const span = plugin . tracer . startSpan (
251
- name ,
252
- {
253
- kind : SpanKind . SERVER ,
254
- attributes : {
255
- [ SEMATTRS_FAAS_EXECUTION ] : context . awsRequestId ,
256
- [ SEMRESATTRS_FAAS_ID ] : context . invokedFunctionArn ,
257
- [ SEMRESATTRS_CLOUD_ACCOUNT_ID ] : AwsLambdaInstrumentation . _extractAccountId ( context . invokedFunctionArn ) ,
258
- [ ATTR_FAAS_COLDSTART ] : requestIsColdStart ,
259
- ...AwsLambdaInstrumentation . _extractOtherEventFields ( event ) ,
260
- } ,
261
- } ,
262
- parent ,
263
- ) ;
264
-
265
- const { requestHook } = config ;
266
- if ( requestHook ) {
267
- safeExecuteInTheMiddle (
268
- ( ) => requestHook ( span , { event, context } ) ,
269
- e => {
270
- if ( e ) diag . error ( 'aws-lambda instrumentation: requestHook error' , e ) ;
271
- } ,
272
- true ,
273
- ) ;
274
- }
310
+ const span = plugin . _createSpanForRequest ( event , context , requestIsColdStart , parent ) ;
311
+ plugin . _applyRequestHook ( span , event , context ) ;
275
312
276
313
return otelContext . with ( trace . setSpan ( parent , span ) , ( ) => {
277
314
// Lambda seems to pass a callback even if handler is of Promise form, so we wrap all the time before calling
@@ -289,23 +326,80 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
289
326
}
290
327
} ,
291
328
) as Promise < { } > | undefined ;
292
- if ( typeof maybePromise ?. then === 'function' ) {
293
- return maybePromise . then (
294
- value => {
295
- plugin . _applyResponseHook ( span , null , value ) ;
296
- return new Promise ( resolve => plugin . _endSpan ( span , undefined , ( ) => resolve ( value ) ) ) ;
297
- } ,
298
- ( err : Error | string ) => {
299
- plugin . _applyResponseHook ( span , err ) ;
300
- return new Promise ( ( resolve , reject ) => plugin . _endSpan ( span , err , ( ) => reject ( err ) ) ) ;
301
- } ,
302
- ) ;
303
- }
304
- return maybePromise ;
329
+
330
+ return plugin . _handlePromiseResult ( span , maybePromise ) ;
305
331
} ) ;
306
332
} ;
307
333
}
308
334
335
+ private _createSpanForRequest ( event : any , context : Context , requestIsColdStart : boolean , parent : OtelContext ) : Span {
336
+ const name = context . functionName ;
337
+ return this . tracer . startSpan (
338
+ name ,
339
+ {
340
+ kind : SpanKind . SERVER ,
341
+ attributes : {
342
+ [ SEMATTRS_FAAS_EXECUTION ] : context . awsRequestId ,
343
+ [ SEMRESATTRS_FAAS_ID ] : context . invokedFunctionArn ,
344
+ [ SEMRESATTRS_CLOUD_ACCOUNT_ID ] : AwsLambdaInstrumentation . _extractAccountId ( context . invokedFunctionArn ) ,
345
+ [ ATTR_FAAS_COLDSTART ] : requestIsColdStart ,
346
+ ...AwsLambdaInstrumentation . _extractOtherEventFields ( event ) ,
347
+ } ,
348
+ } ,
349
+ parent ,
350
+ ) ;
351
+ }
352
+
353
+ private _applyRequestHook ( span : Span , event : any , context : Context ) : void {
354
+ const { requestHook } = this . getConfig ( ) ;
355
+ if ( requestHook ) {
356
+ safeExecuteInTheMiddle (
357
+ ( ) => requestHook ( span , { event, context } ) ,
358
+ e => {
359
+ if ( e ) diag . error ( 'aws-lambda instrumentation: requestHook error' , e ) ;
360
+ } ,
361
+ true ,
362
+ ) ;
363
+ }
364
+ }
365
+
366
+ private _handlePromiseResult ( span : Span , maybePromise : Promise < { } > | undefined ) : Promise < { } > | undefined {
367
+ if ( typeof maybePromise ?. then === 'function' ) {
368
+ return maybePromise . then (
369
+ value => {
370
+ this . _applyResponseHook ( span , null , value ) ;
371
+ return new Promise ( resolve => this . _endSpan ( span , undefined , ( ) => resolve ( value ) ) ) ;
372
+ } ,
373
+ ( err : Error | string ) => {
374
+ this . _applyResponseHook ( span , err ) ;
375
+ return new Promise ( ( resolve , reject ) => this . _endSpan ( span , err , ( ) => reject ( err ) ) ) ;
376
+ } ,
377
+ ) ;
378
+ }
379
+
380
+ // Handle synchronous return values by ending the span and applying response hook
381
+ this . _applyResponseHook ( span , null , maybePromise ) ;
382
+ this . _endSpan ( span , undefined , ( ) => { } ) ;
383
+ return maybePromise ;
384
+ }
385
+
386
+ private _determineParent ( event : any , context : Context ) : OtelContext {
387
+ const config = this . getConfig ( ) ;
388
+ return AwsLambdaInstrumentation . _determineParent (
389
+ event ,
390
+ context ,
391
+ config . eventContextExtractor || AwsLambdaInstrumentation . _defaultEventContextExtractor ,
392
+ ) ;
393
+ }
394
+
395
+ private _isStreamingHandler < TEvent , TResult > (
396
+ handler : Handler < TEvent , TResult > | StreamifyHandler < TEvent , TResult > ,
397
+ ) : handler is StreamifyHandler < TEvent , TResult > {
398
+ return (
399
+ ( handler as unknown as Record < symbol , unknown > ) [ AWS_HANDLER_STREAMING_SYMBOL ] === AWS_HANDLER_STREAMING_RESPONSE
400
+ ) ;
401
+ }
402
+
309
403
/**
310
404
*
311
405
*/
0 commit comments