@@ -36,6 +36,7 @@ import { SecretsManagerServiceExtension } from './aws/services/secretsmanager';
3636import { StepFunctionsServiceExtension } from './aws/services/step-functions' ;
3737import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda' ;
3838import type { Command as AwsV3Command } from '@aws-sdk/types' ;
39+ import { suppressTracing } from '@opentelemetry/core' ;
3940
4041export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID' ;
4142export const AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = 'X-Amzn-Trace-Id' ;
@@ -364,67 +365,89 @@ const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.clien
364365type V3PluginCommand = AwsV3Command < any , any , any , any , any > & {
365366 [ V3_CLIENT_CONFIG_KEY ] ?: any ;
366367} ;
368+ // Symbol to prevent infinite recursion during credential capture
369+ // When we extract credentials, the AWS SDK may need to make additional AWS API calls
370+ // (e.g., sts:AssumeRoleWithWebIdentity) which go through the same instrumented 'send' method.
371+ // Without this flag, each credential request would trigger another credential extraction attempt,
372+ // creating an infinite loop of nested AWS SDK calls.
373+ const SKIP_CREDENTIAL_CAPTURE_KEY = Symbol ( 'skip-credential-capture' ) ;
367374function patchAwsSdkInstrumentation ( instrumentation : Instrumentation ) : void {
368375 if ( instrumentation ) {
369376 ( instrumentation as AwsInstrumentation ) [ '_getV3SmithyClientSendPatch' ] = function (
370377 original : ( ...args : unknown [ ] ) => Promise < any >
371378 ) {
372379 return function send ( this : any , command : V3PluginCommand , ...args : unknown [ ] ) : Promise < any > {
373- this . middlewareStack ?. add (
374- ( next : any , context : any ) => async ( middlewareArgs : any ) => {
375- propagation . inject ( otelContext . active ( ) , middlewareArgs . request . headers , defaultTextMapSetter ) ;
376- // Need to set capitalized version of the trace id to ensure that the Recursion Detection Middleware
377- // of aws-sdk-js-v3 will detect the propagated X-Ray Context
378- // See: https://github.com/aws/aws-sdk-js-v3/blob/v3.768.0/packages/middleware-recursion-detection/src/index.ts#L13
379- const xrayTraceId = middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
380-
381- if ( xrayTraceId ) {
382- middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER_CAPITALIZED ] = xrayTraceId ;
383- delete middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
380+ // Only add middleware once per client instance to reduce overhead
381+ // AWS SDK clients may call 'send' multiple times, but we only need to patch once
382+ // Even with override=true, adding middleware still causes overhead as it replaces existing stack entries
383+ if ( ! this . __adotMiddlewarePatched ) {
384+ this . middlewareStack ?. add (
385+ ( next : any , context : any ) => async ( middlewareArgs : any ) => {
386+ propagation . inject ( otelContext . active ( ) , middlewareArgs . request . headers , defaultTextMapSetter ) ;
387+ // Need to set capitalized version of the trace id to ensure that the Recursion Detection Middleware
388+ // of aws-sdk-js-v3 will detect the propagated X-Ray Context
389+ // See: https://github.com/aws/aws-sdk-js-v3/blob/v3.768.0/packages/middleware-recursion-detection/src/index.ts#L13
390+ const xrayTraceId = middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
391+
392+ if ( xrayTraceId ) {
393+ middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER_CAPITALIZED ] = xrayTraceId ;
394+ delete middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
395+ }
396+ const result = await next ( middlewareArgs ) ;
397+ return result ;
398+ } ,
399+ {
400+ step : 'build' ,
401+ name : '_adotInjectXrayContextMiddleware' ,
402+ override : true ,
384403 }
385- const result = await next ( middlewareArgs ) ;
386- return result ;
387- } ,
388- {
389- step : 'build' ,
390- name : '_adotInjectXrayContextMiddleware' ,
391- override : true ,
392- }
393- ) ;
394-
395- this . middlewareStack ?. add (
396- ( next : any , context : any ) => async ( middlewareArgs : any ) => {
397- const activeContext = otelContext . active ( ) ;
398- const span = trace . getSpan ( activeContext ) ;
399-
400- if ( span ) {
401- try {
402- const credsProvider = this . config . credentials ;
403- if ( credsProvider instanceof Function ) {
404- const credentials = await credsProvider ( ) ;
405- if ( credentials ?. accessKeyId ) {
406- span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_ACCOUNT_ACCESS_KEY , credentials . accessKeyId ) ;
407- }
408- }
409- if ( this . config . region instanceof Function ) {
410- const region = await this . config . region ( ) ;
411- if ( region ) {
412- span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_REGION , region ) ;
404+ ) ;
405+
406+ this . middlewareStack ?. add (
407+ ( next : any , context : any ) => async ( middlewareArgs : any ) => {
408+ const activeContext = otelContext . active ( ) ;
409+ // Skip credential extraction if this is a nested call from another credential extraction
410+ // This prevents infinite recursion when credential providers make AWS API calls
411+ if ( activeContext . getValue ( SKIP_CREDENTIAL_CAPTURE_KEY ) ) {
412+ return await next ( middlewareArgs ) ;
413+ }
414+ const span = trace . getSpan ( activeContext ) ;
415+
416+ if ( span ) {
417+ // suppressTracing prevents span generation for internal credential extraction calls
418+ // which are implementation details and not relevant to the application's telemetry
419+ const suppressedContext = suppressTracing ( activeContext ) . setValue ( SKIP_CREDENTIAL_CAPTURE_KEY , true ) ;
420+ await otelContext . with ( suppressedContext , async ( ) => {
421+ try {
422+ const credsProvider = this . config . credentials ;
423+ if ( credsProvider instanceof Function ) {
424+ const credentials = await credsProvider ( ) ;
425+ if ( credentials ?. accessKeyId ) {
426+ span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_ACCOUNT_ACCESS_KEY , credentials . accessKeyId ) ;
427+ }
428+ }
429+ if ( this . config . region instanceof Function ) {
430+ const region = await this . config . region ( ) ;
431+ if ( region ) {
432+ span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_REGION , region ) ;
433+ }
434+ }
435+ } catch ( err ) {
436+ diag . debug ( 'Failed to get auth account access key and region:' , err ) ;
413437 }
414- }
415- } catch ( err ) {
416- diag . debug ( 'Failed to get auth account access key and region:' , err ) ;
438+ } ) ;
417439 }
418- }
419440
420- return await next ( middlewareArgs ) ;
421- } ,
422- {
423- step : 'build' ,
424- name : '_adotExtractSignerCredentials' ,
425- override : true ,
426- }
427- ) ;
441+ return await next ( middlewareArgs ) ;
442+ } ,
443+ {
444+ step : 'build' ,
445+ name : '_adotExtractSignerCredentials' ,
446+ override : true ,
447+ }
448+ ) ;
449+ this . __adotMiddlewarePatched = true ;
450+ }
428451
429452 command [ V3_CLIENT_CONFIG_KEY ] = this . config ;
430453 return original . apply ( this , [ command , ...args ] ) ;
0 commit comments