Skip to content

Commit 0c2c969

Browse files
Extract arn and account access key
1 parent a02218c commit 0c2c969

File tree

3 files changed

+180
-2
lines changed

3 files changed

+180
-2
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-attribute-keys.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import { SEMATTRS_AWS_DYNAMODB_TABLE_NAMES } from '@opentelemetry/semantic-conve
55

66
// Utility class holding attribute keys with special meaning to AWS components
77
export const AWS_ATTRIBUTE_KEYS = {
8+
AWS_AUTH_ACCESS_KEY: 'aws.auth.account.access_key',
9+
AWS_AUTH_REGION: 'aws.auth.region',
810
AWS_SPAN_KIND: 'aws.span.kind',
911
AWS_LOCAL_SERVICE: 'aws.local.service',
1012
AWS_LOCAL_OPERATION: 'aws.local.operation',
1113
AWS_REMOTE_SERVICE: 'aws.remote.service',
1214
AWS_REMOTE_ENVIRONMENT: 'aws.remote.environment',
1315
AWS_REMOTE_OPERATION: 'aws.remote.operation',
16+
AWS_REMOTE_RESOURCE_ACCESS_KEY: 'aws.remote.resource.account.access_key',
17+
AWS_REMOTE_RESOURCE_ACCOUNT_ID: 'aws.remote.resource.account.id',
18+
AWS_REMOTE_RESOURCE_REGION: 'aws.remote.resource.region',
1419
AWS_REMOTE_RESOURCE_TYPE: 'aws.remote.resource.type',
1520
AWS_REMOTE_RESOURCE_IDENTIFIER: 'aws.remote.resource.identifier',
1621
AWS_SDK_DESCENDANT: 'aws.sdk.descendant',
@@ -31,7 +36,9 @@ export const AWS_ATTRIBUTE_KEYS = {
3136
AWS_S3_BUCKET: 'aws.s3.bucket',
3237
AWS_SQS_QUEUE_URL: 'aws.sqs.queue.url',
3338
AWS_SQS_QUEUE_NAME: 'aws.sqs.queue.name',
39+
AWS_KINESIS_STREAM_ARN: 'aws.kinesis.stream.arn',
3440
AWS_KINESIS_STREAM_NAME: 'aws.kinesis.stream.name',
41+
AWS_DYNAMODB_TABLE_ARN: 'aws.dynamodb.table.arn',
3542
AWS_DYNAMODB_TABLE_NAMES: SEMATTRS_AWS_DYNAMODB_TABLE_NAMES,
3643
AWS_BEDROCK_DATA_SOURCE_ID: 'aws.bedrock.data_source.id',
3744
AWS_BEDROCK_KNOWLEDGE_BASE_ID: 'aws.bedrock.knowledge_base.id',

aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export function applyInstrumentationPatches(instrumentations: Instrumentation[])
8080
patchSqsServiceExtension(services.get('SQS'));
8181
patchSnsServiceExtension(services.get('SNS'));
8282
patchLambdaServiceExtension(services.get('Lambda'));
83+
patchKinesisServiceExtension(services.get('Kinesis'));
84+
patchDynamoDbServiceExtension(services.get('DynamoDB'));
8385
}
8486
} else if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-lambda') {
8587
diag.debug('Patching aws lambda instrumentation');
@@ -189,6 +191,68 @@ function patchSnsServiceExtension(snsServiceExtension: any): void {
189191
}
190192
}
191193

194+
/*
195+
* This patch extends the existing upstream extension for Kinesis. Extensions allow for custom logic for adding
196+
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
197+
* `aws.kinesis.stream.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation.
198+
*
199+
*
200+
* @param kinesisServiceExtension Kinesis Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
201+
*/
202+
function patchKinesisServiceExtension(kinesisServiceExtension: any): void {
203+
if (kinesisServiceExtension) {
204+
const requestPreSpanHook = kinesisServiceExtension.requestPreSpanHook;
205+
kinesisServiceExtension._requestPreSpanHook = requestPreSpanHook;
206+
207+
const patchedRequestPreSpanHook = (
208+
request: NormalizedRequest,
209+
_config: AwsSdkInstrumentationConfig
210+
): RequestMetadata => {
211+
const requestMetadata: RequestMetadata = kinesisServiceExtension._requestPreSpanHook(request, _config);
212+
if (requestMetadata.spanAttributes) {
213+
const streamArn = request.commandInput?.StreamARN;
214+
if (streamArn) {
215+
requestMetadata.spanAttributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN] = streamArn;
216+
}
217+
}
218+
return requestMetadata;
219+
};
220+
221+
kinesisServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
222+
}
223+
}
224+
225+
/*
226+
* This patch extends the existing upstream extension for DynamoDB. Extensions allow for custom logic for adding
227+
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
228+
* `aws.dynamodb.table.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation.
229+
*
230+
*
231+
* @param dynamoDbServiceExtension DynamoDB Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
232+
*/
233+
function patchDynamoDbServiceExtension(dynamoDbServiceExtension: any): void {
234+
if (dynamoDbServiceExtension) {
235+
if (typeof dynamoDbServiceExtension.responseHook === 'function') {
236+
const originalResponseHook = dynamoDbServiceExtension.responseHook;
237+
dynamoDbServiceExtension.responseHook = (
238+
response: NormalizedResponse,
239+
span: Span,
240+
tracer: Tracer,
241+
config: AwsSdkInstrumentationConfig
242+
): void => {
243+
originalResponseHook.call(dynamoDbServiceExtension, response, span, tracer, config);
244+
245+
if (response.data && response.data.Table) {
246+
const tableArn = response.data.Table.TableArn;
247+
if (tableArn) {
248+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN, tableArn);
249+
}
250+
}
251+
};
252+
}
253+
}
254+
}
255+
192256
/*
193257
* This patch extends the existing upstream extension for Lambda. Extensions allow for custom logic for adding
194258
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
@@ -293,7 +357,7 @@ function patchAwsLambdaInstrumentation(instrumentation: Instrumentation): void {
293357
}
294358
}
295359

296-
// Override the upstream private _getV3SmithyClientSendPatch method to add middleware to inject X-Ray Trace Context into HTTP Headers
360+
// Override the upstream private _getV3SmithyClientSendPatch method to add middlewares to inject X-Ray Trace Context into HTTP Headers and to extract account access key id and region for cross-account support
297361
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/instrumentation-aws-sdk-v0.48.0/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts#L373-L384
298362
const awsXrayPropagator = new AWSXRayPropagator();
299363
const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.client.config');
@@ -328,6 +392,40 @@ function patchAwsSdkInstrumentation(instrumentation: Instrumentation): void {
328392
}
329393
);
330394

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) {
404+
const credentials = await credsProvider();
405+
if (credentials.accessKeyId) {
406+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY, credentials.accessKeyId);
407+
}
408+
}
409+
if (this.config.region) {
410+
const region = await this.config.region();
411+
if (region) {
412+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, region);
413+
}
414+
}
415+
} catch (err) {
416+
diag.debug('Fail to get auth account access key and region.');
417+
}
418+
}
419+
420+
return await next(middlewareArgs);
421+
},
422+
{
423+
step: 'build',
424+
name: '_extractSignerCredentials',
425+
override: true,
426+
}
427+
);
428+
331429
command[V3_CLIENT_CONFIG_KEY] = this.config;
332430
return original.apply(this, [command, ...args]);
333431
};

aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const _BEDROCK_GUARDRAIL_ARN: string = 'arn:aws:bedrock:us-east-1:123456789012:g
5858
const _BEDROCK_KNOWLEDGEBASE_ID: string = 'KnowledgeBaseId';
5959
const _GEN_AI_SYSTEM: string = 'aws.bedrock';
6060
const _GEN_AI_REQUEST_MODEL: string = 'genAiReuqestModelId';
61+
const _STREAM_ARN: string = 'arn:aws:kinesis:us-west-2:123456789012:stream/testStream';
62+
const _TABLE_ARN: string = 'arn:aws:dynamodb:us-west-2:123456789012:table/testTable';
6163

6264
const mockHeaders = {
6365
'x-test-header': 'test-value',
@@ -90,6 +92,8 @@ describe('InstrumentationPatchTest', () => {
9092
expect(services.get('Lambda').requestPreSpanHook).toBeTruthy();
9193
expect(services.get('SQS')._requestPreSpanHook).toBeFalsy();
9294
expect(services.get('SQS').requestPreSpanHook).toBeTruthy();
95+
expect(services.get('Kinesis')._requestPreSpanHook).toBeFalsy();
96+
expect(services.get('Kinesis').requestPreSpanHook).toBeTruthy();
9397
expect(services.has('Bedrock')).toBeFalsy();
9498
expect(services.has('BedrockAgent')).toBeFalsy();
9599
expect(services.get('BedrockAgentRuntime')).toBeFalsy();
@@ -119,6 +123,8 @@ describe('InstrumentationPatchTest', () => {
119123
expect(services.get('Lambda').requestPreSpanHook).toBeTruthy();
120124
expect(services.get('SQS')._requestPreSpanHook).toBeTruthy();
121125
expect(services.get('SQS').requestPreSpanHook).toBeTruthy();
126+
expect(services.get('Kinesis')._requestPreSpanHook).toBeTruthy();
127+
expect(services.get('Kinesis').requestPreSpanHook).toBeTruthy();
122128
expect(services.has('Bedrock')).toBeTruthy();
123129
expect(services.has('BedrockAgent')).toBeTruthy();
124130
expect(services.get('BedrockAgentRuntime')).toBeTruthy();
@@ -179,6 +185,14 @@ describe('InstrumentationPatchTest', () => {
179185
expect(() => doExtractBedrockAttributes(services, 'Bedrock')).toThrow();
180186
});
181187

188+
it('Kinesis without patching', () => {
189+
const unpatchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(UNPATCHED_INSTRUMENTATIONS);
190+
const services: Map<string, any> = extractServicesFromAwsSdkInstrumentation(unpatchedAwsSdkInstrumentation);
191+
expect(() => doExtractKinesisAttributes(services)).not.toThrow();
192+
const kinesisAttributes: Attributes = doExtractKinesisAttributes(services);
193+
expect(kinesisAttributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN]).toBeUndefined();
194+
});
195+
182196
it('SNS with patching', () => {
183197
const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS);
184198
const services: Map<string, any> = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation);
@@ -233,6 +247,20 @@ describe('InstrumentationPatchTest', () => {
233247
expect(responseHookSecretsManagerAttributes[AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN]).toBe(_SECRETS_ARN);
234248
});
235249

250+
it('Kinesis with patching', () => {
251+
const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS);
252+
const services: Map<string, any> = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation);
253+
const requestKinesisAttributes: Attributes = doExtractKinesisAttributes(services);
254+
expect(requestKinesisAttributes[AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN]).toEqual(_STREAM_ARN);
255+
});
256+
257+
it('DynamoDB with patching', () => {
258+
const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS);
259+
const services: Map<string, any> = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation);
260+
const responseDynamoDbAttributes: Attributes = doResponseHookDynamoDb(services);
261+
expect(responseDynamoDbAttributes[AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN]).toEqual(_TABLE_ARN);
262+
});
263+
236264
it('Bedrock with patching', () => {
237265
const patchedAwsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS);
238266
const services: Map<string, any> = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation);
@@ -446,6 +474,18 @@ describe('InstrumentationPatchTest', () => {
446474
return doExtractAttributes(services, serviceName, params);
447475
}
448476

477+
function doExtractKinesisAttributes(services: Map<string, ServiceExtension>): Attributes {
478+
const serviceName: string = 'Kinesis';
479+
const params: NormalizedRequest = {
480+
serviceName: serviceName,
481+
commandName: 'mockCommandName',
482+
commandInput: {
483+
StreamARN: _STREAM_ARN,
484+
},
485+
};
486+
return doExtractAttributes(services, serviceName, params);
487+
}
488+
449489
function doExtractAttributes(
450490
services: Map<string, ServiceExtension>,
451491
serviceName: string,
@@ -492,6 +532,23 @@ describe('InstrumentationPatchTest', () => {
492532
return doResponseHook(services, 'Lambda', results as NormalizedResponse);
493533
}
494534

535+
function doResponseHookDynamoDb(services: Map<string, ServiceExtension>): Attributes {
536+
const results: Partial<NormalizedResponse> = {
537+
data: {
538+
Table: {
539+
TableArn: _TABLE_ARN,
540+
},
541+
},
542+
request: {
543+
commandInput: {},
544+
commandName: 'dummy_operation',
545+
serviceName: 'DynamoDB',
546+
},
547+
};
548+
549+
return doResponseHook(services, 'DynamoDB', results as NormalizedResponse);
550+
}
551+
495552
function doResponseHookBedrock(
496553
services: Map<string, ServiceExtension>,
497554
serviceName: string,
@@ -563,9 +620,13 @@ describe('InstrumentationPatchTest', () => {
563620
mockedMiddlewareStack = {
564621
add: (arg1: any, arg2: any) => mockedMiddlewareStackInternal.push([arg1, arg2]),
565622
};
623+
const mockConfig = {
624+
credentials: () => Promise.resolve({ accessKeyId: 'test-access-key' }),
625+
region: () => Promise.resolve('us-west-2'),
626+
};
566627
const send = extractAwsSdkInstrumentation(PATCHED_INSTRUMENTATIONS)
567628
['_getV3SmithyClientSendPatch']((...args: unknown[]) => Promise.resolve())
568-
.bind({ middlewareStack: mockedMiddlewareStack });
629+
.bind({ middlewareStack: mockedMiddlewareStack, config: mockConfig });
569630

570631
middlewareArgsHeader = {
571632
request: {
@@ -617,6 +678,18 @@ describe('InstrumentationPatchTest', () => {
617678

618679
expect(mockedMiddlewareStackInternal[0][1].name).toEqual('_adotInjectXrayContextMiddleware');
619680
});
681+
682+
it('Add cross account information span attributes from STS credentials', async () => {
683+
const mockSpan = { setAttribute: sinon.stub() };
684+
sinon.stub(trace, 'getSpan').returns(mockSpan as unknown as Span);
685+
const credentialsMiddlewareArgs: any = {};
686+
await mockedMiddlewareStackInternal[1][0]((arg: any) => Promise.resolve(arg), null)(credentialsMiddlewareArgs);
687+
expect(mockedMiddlewareStackInternal[1][1].name).toEqual('_extractSignerCredentials');
688+
expect(
689+
mockSpan.setAttribute.calledWith(AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCESS_KEY, 'test-access-key')
690+
).toBeTruthy();
691+
expect(mockSpan.setAttribute.calledWith(AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, 'us-west-2')).toBeTruthy();
692+
});
620693
});
621694

622695
it('injects trace context header into request via propagator', async () => {

0 commit comments

Comments
 (0)