@@ -37,6 +37,7 @@ import { StepFunctionsServiceExtension } from './aws/services/step-functions';
3737import type { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda' ;
3838import type { Command as AwsV3Command } from '@aws-sdk/types' ;
3939import { LoggerProvider } from '@opentelemetry/api-logs' ;
40+ import { suppressTracing } from '@opentelemetry/core' ;
4041
4142export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID' ;
4243export const AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = 'X-Amzn-Trace-Id' ;
@@ -417,67 +418,89 @@ const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.clien
417418type V3PluginCommand = AwsV3Command < any , any , any , any , any > & {
418419 [ V3_CLIENT_CONFIG_KEY ] ?: any ;
419420} ;
421+ // Symbol to prevent infinite recursion during credential capture
422+ // When we extract credentials, the AWS SDK may need to make additional AWS API calls
423+ // (e.g., sts:AssumeRoleWithWebIdentity) which go through the same instrumented 'send' method.
424+ // Without this flag, each credential request would trigger another credential extraction attempt,
425+ // creating an infinite loop of nested AWS SDK calls.
426+ const SKIP_CREDENTIAL_CAPTURE_KEY = Symbol ( 'skip-credential-capture' ) ;
420427function patchAwsSdkInstrumentation ( instrumentation : Instrumentation ) : void {
421428 if ( instrumentation ) {
422429 ( instrumentation as AwsInstrumentation ) [ '_getV3SmithyClientSendPatch' ] = function (
423430 original : ( ...args : unknown [ ] ) => Promise < any >
424431 ) {
425432 return function send ( this : any , command : V3PluginCommand , ...args : unknown [ ] ) : Promise < any > {
426- this . middlewareStack ?. add (
427- ( next : any , context : any ) => async ( middlewareArgs : any ) => {
428- propagation . inject ( otelContext . active ( ) , middlewareArgs . request . headers , defaultTextMapSetter ) ;
429- // Need to set capitalized version of the trace id to ensure that the Recursion Detection Middleware
430- // of aws-sdk-js-v3 will detect the propagated X-Ray Context
431- // See: https://github.com/aws/aws-sdk-js-v3/blob/v3.768.0/packages/middleware-recursion-detection/src/index.ts#L13
432- const xrayTraceId = middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
433-
434- if ( xrayTraceId ) {
435- middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER_CAPITALIZED ] = xrayTraceId ;
436- delete middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
433+ // Only add middleware once per client instance to reduce overhead
434+ // AWS SDK clients may call 'send' multiple times, but we only need to patch once
435+ // Even with override=true, adding middleware still causes overhead as it replaces existing stack entries
436+ if ( ! this . __adotMiddlewarePatched ) {
437+ this . middlewareStack ?. add (
438+ ( next : any , context : any ) => async ( middlewareArgs : any ) => {
439+ propagation . inject ( otelContext . active ( ) , middlewareArgs . request . headers , defaultTextMapSetter ) ;
440+ // Need to set capitalized version of the trace id to ensure that the Recursion Detection Middleware
441+ // of aws-sdk-js-v3 will detect the propagated X-Ray Context
442+ // See: https://github.com/aws/aws-sdk-js-v3/blob/v3.768.0/packages/middleware-recursion-detection/src/index.ts#L13
443+ const xrayTraceId = middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
444+
445+ if ( xrayTraceId ) {
446+ middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER_CAPITALIZED ] = xrayTraceId ;
447+ delete middlewareArgs . request . headers [ AWSXRAY_TRACE_ID_HEADER ] ;
448+ }
449+ const result = await next ( middlewareArgs ) ;
450+ return result ;
451+ } ,
452+ {
453+ step : 'build' ,
454+ name : '_adotInjectXrayContextMiddleware' ,
455+ override : true ,
437456 }
438- const result = await next ( middlewareArgs ) ;
439- return result ;
440- } ,
441- {
442- step : 'build' ,
443- name : '_adotInjectXrayContextMiddleware' ,
444- override : true ,
445- }
446- ) ;
447-
448- this . middlewareStack ?. add (
449- ( next : any , context : any ) => async ( middlewareArgs : any ) => {
450- const activeContext = otelContext . active ( ) ;
451- const span = trace . getSpan ( activeContext ) ;
452-
453- if ( span ) {
454- try {
455- const credsProvider = this . config . credentials ;
456- if ( credsProvider instanceof Function ) {
457- const credentials = await credsProvider ( ) ;
458- if ( credentials ?. accessKeyId ) {
459- span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_ACCOUNT_ACCESS_KEY , credentials . accessKeyId ) ;
460- }
461- }
462- if ( this . config . region instanceof Function ) {
463- const region = await this . config . region ( ) ;
464- if ( region ) {
465- span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_REGION , region ) ;
457+ ) ;
458+
459+ this . middlewareStack ?. add (
460+ ( next : any , context : any ) => async ( middlewareArgs : any ) => {
461+ const activeContext = otelContext . active ( ) ;
462+ // Skip credential extraction if this is a nested call from another credential extraction
463+ // This prevents infinite recursion when credential providers make AWS API calls
464+ if ( activeContext . getValue ( SKIP_CREDENTIAL_CAPTURE_KEY ) ) {
465+ return await next ( middlewareArgs ) ;
466+ }
467+ const span = trace . getSpan ( activeContext ) ;
468+
469+ if ( span ) {
470+ // suppressTracing prevents span generation for internal credential extraction calls
471+ // which are implementation details and not relevant to the application's telemetry
472+ const suppressedContext = suppressTracing ( activeContext ) . setValue ( SKIP_CREDENTIAL_CAPTURE_KEY , true ) ;
473+ await otelContext . with ( suppressedContext , async ( ) => {
474+ try {
475+ const credsProvider = this . config . credentials ;
476+ if ( credsProvider instanceof Function ) {
477+ const credentials = await credsProvider ( ) ;
478+ if ( credentials ?. accessKeyId ) {
479+ span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_ACCOUNT_ACCESS_KEY , credentials . accessKeyId ) ;
480+ }
481+ }
482+ if ( this . config . region instanceof Function ) {
483+ const region = await this . config . region ( ) ;
484+ if ( region ) {
485+ span . setAttribute ( AWS_ATTRIBUTE_KEYS . AWS_AUTH_REGION , region ) ;
486+ }
487+ }
488+ } catch ( err ) {
489+ diag . debug ( 'Failed to get auth account access key and region:' , err ) ;
466490 }
467- }
468- } catch ( err ) {
469- diag . debug ( 'Failed to get auth account access key and region:' , err ) ;
491+ } ) ;
470492 }
471- }
472493
473- return await next ( middlewareArgs ) ;
474- } ,
475- {
476- step : 'build' ,
477- name : '_adotExtractSignerCredentials' ,
478- override : true ,
479- }
480- ) ;
494+ return await next ( middlewareArgs ) ;
495+ } ,
496+ {
497+ step : 'build' ,
498+ name : '_adotExtractSignerCredentials' ,
499+ override : true ,
500+ }
501+ ) ;
502+ this . __adotMiddlewarePatched = true ;
503+ }
481504
482505 command [ V3_CLIENT_CONFIG_KEY ] = this . config ;
483506 return original . apply ( this , [ command , ...args ] ) ;
0 commit comments