Skip to content

Commit 5db6a18

Browse files
authored
Lambda Support Refactor + Enhance Unit Test Coverage for AWS Resources (#189)
## What does this pull request do? Refactors changes made for AWS Lambda support to make the more maintainable and easier to reason about. These changes also improve our test coverage to assert these new Lambda behaviors as well as existing behaviors for other AWS Resources (S3, DDB, SNS, etc.). By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 1bf9407 commit 5db6a18

File tree

3 files changed

+251
-34
lines changed

3 files changed

+251
-34
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
SERVICE_METRIC,
3434
} from './metric-attribute-generator';
3535
import { SqsUrlParser } from './sqs-url-parser';
36+
import { LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT } from './aws-opentelemetry-configurator';
3637

3738
// Does not exist in @opentelemetry/semantic-conventions
3839
const _SERVER_SOCKET_ADDRESS: string = 'server.socket.address';
@@ -50,6 +51,9 @@ const _GRAPHQL_OPERATION_TYPE: string = 'graphql.operation.type';
5051
// Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
5152
const GRAPHQL: string = 'graphql';
5253

54+
// Constants for Lambda operations
55+
const LAMBDA_INVOKE_OPERATION: string = 'Invoke';
56+
5357
// Normalized remote service names for supported AWS services
5458
const NORMALIZED_DYNAMO_DB_SERVICE_NAME: string = 'AWS::DynamoDB';
5559
const NORMALIZED_KINESIS_SERVICE_NAME: string = 'AWS::Kinesis';
@@ -109,6 +113,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
109113
AwsMetricAttributeGenerator.setEgressOperation(span, attributes);
110114
AwsMetricAttributeGenerator.setRemoteServiceAndOperation(span, attributes);
111115
AwsMetricAttributeGenerator.setRemoteResourceTypeAndIdentifier(span, attributes);
116+
AwsMetricAttributeGenerator.setRemoteEnvironment(span, attributes);
112117
AwsMetricAttributeGenerator.setSpanKindForDependency(span, attributes);
113118
AwsMetricAttributeGenerator.setRemoteDbUser(span, attributes);
114119

@@ -336,7 +341,18 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
336341
BedrockRuntime: NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME,
337342
SecretsManager: NORMALIZED_SECRETSMANAGER_SERVICE_NAME,
338343
SFN: NORMALIZED_STEPFUNCTIONS_SERVICE_NAME,
344+
Lambda: NORMALIZED_LAMBDA_SERVICE_NAME,
339345
};
346+
347+
// Special handling for Lambda invoke operations
348+
if (AwsMetricAttributeGenerator.isLambdaInvokeOperation(span)) {
349+
const lambdaFunctionName = span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME];
350+
// If Lambda name is not present, use UnknownRemoteService
351+
// This is intentional - we want to clearly indicate when the Lambda function name
352+
// is missing rather than falling back to a generic service name
353+
return lambdaFunctionName ? String(lambdaFunctionName) : AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE;
354+
}
355+
340356
return awsSdkServiceMapping[serviceName] || 'AWS::' + serviceName;
341357
}
342358
return serviceName;
@@ -410,24 +426,9 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
410426
);
411427
cloudFormationIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(activityArn);
412428
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME)) {
413-
// Handling downstream Lambda as a service vs. an AWS resource:
414-
// - If the method call is "Invoke", we treat downstream Lambda as a service.
415-
// - Otherwise, we treat it as an AWS resource.
416-
//
417-
// This addresses a Lambda topology issue in Application Signals.
418-
// More context in PR: https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
419-
//
420-
// NOTE: The env var LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT was introduced as part of this fix.
421-
// It is optional and allow users to override the default value if needed.
422-
if (AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_RPC_METHOD) === 'Invoke') {
423-
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE] = AwsMetricAttributeGenerator.escapeDelimiters(
424-
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME]
425-
);
426-
427-
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT] = `lambda:${
428-
process.env.LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT || 'default'
429-
}`;
430-
} else {
429+
// For non-Invoke Lambda operations, treat Lambda as a resource,
430+
// see normalizeRemoteServiceName for more information.
431+
if (!AwsMetricAttributeGenerator.isLambdaInvokeOperation(span)) {
431432
remoteResourceType = NORMALIZED_LAMBDA_SERVICE_NAME + '::Function';
432433
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
433434
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME]
@@ -491,17 +492,33 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
491492
remoteResourceIdentifier = AwsMetricAttributeGenerator.getDbConnection(span);
492493
}
493494

495+
if (cloudFormationIdentifier === undefined) {
496+
cloudFormationIdentifier = remoteResourceIdentifier;
497+
}
498+
494499
if (remoteResourceType !== undefined && remoteResourceIdentifier !== undefined) {
495500
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_TYPE] = remoteResourceType;
496501
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_RESOURCE_IDENTIFIER] = remoteResourceIdentifier;
502+
attributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudFormationIdentifier;
503+
}
504+
}
497505

498-
if (AwsSpanProcessingUtil.isAwsSDKSpan(span)) {
499-
if (cloudFormationIdentifier === undefined) {
500-
cloudFormationIdentifier = remoteResourceIdentifier;
501-
}
502-
503-
attributes[AWS_ATTRIBUTE_KEYS.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudFormationIdentifier;
506+
/**
507+
* Remote environment is used to identify the environment of downstream services. Currently only
508+
* set to "lambda:default" for Lambda Invoke operations when aws-api system is detected.
509+
*/
510+
private static setRemoteEnvironment(span: ReadableSpan, attributes: Attributes): void {
511+
// We want to treat downstream Lambdas as a service rather than a resource because
512+
// Application Signals topology map gets disconnected due to conflicting Lambda Entity
513+
// definitions
514+
// Additional context can be found in
515+
// https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
516+
if (AwsMetricAttributeGenerator.isLambdaInvokeOperation(span)) {
517+
let remoteEnvironment = process.env[LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT]?.trim();
518+
if (!remoteEnvironment) {
519+
remoteEnvironment = 'default';
504520
}
521+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT] = `lambda:${remoteEnvironment}`;
505522
}
506523
}
507524

@@ -620,6 +637,18 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
620637
return input.split('^').join('^^').split('|').join('^|');
621638
}
622639

640+
/**
641+
* Check if the span represents a Lambda Invoke operation.
642+
*/
643+
private static isLambdaInvokeOperation(span: ReadableSpan): boolean {
644+
if (!AwsSpanProcessingUtil.isAwsSDKSpan(span)) {
645+
return false;
646+
}
647+
648+
const rpcService = AwsMetricAttributeGenerator.getRemoteService(span, SEMATTRS_RPC_SERVICE);
649+
return rpcService === 'Lambda' && span.attributes[SEMATTRS_RPC_METHOD] === LAMBDA_INVOKE_OPERATION;
650+
}
651+
623652
// Extracts the name of the resource from an arn
624653
private static extractResourceNameFromArn(attribute: AttributeValue | undefined): string | undefined {
625654
if (typeof attribute === 'string' && attribute.startsWith('arn:aws:')) {

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = 'T1U';
7575
// Follow Python SDK Impl to set the max span batch size
7676
// which will reduce the chance of UDP package size is larger than 64KB
7777
const LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10;
78+
export const LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT: string = 'LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT';
7879

7980
/**
8081
* Aws Application Signals Config Provider creates a configuration object that can be provided to

0 commit comments

Comments
 (0)