diff --git a/integration-tests/bin/app.ts b/integration-tests/bin/app.ts index d822e6cac..800991273 100644 --- a/integration-tests/bin/app.ts +++ b/integration-tests/bin/app.ts @@ -6,6 +6,7 @@ import {Otlp} from '../lib/stacks/otlp'; import {Snapstart} from '../lib/stacks/snapstart'; import {LambdaManagedInstancesStack} from '../lib/stacks/lmi'; import {AuthStack} from '../lib/stacks/auth'; +import {Svls8583Stack} from '../lib/stacks/svls-8583'; import {AuthRoleStack} from '../lib/auth-role'; import {ACCOUNT, getIdentifier, REGION} from '../config'; import {CapacityProviderStack} from "../lib/capacity-provider"; @@ -40,6 +41,9 @@ const stacks = [ new AuthStack(app, `integ-${identifier}-auth`, { env, }), + new Svls8583Stack(app, `integ-${identifier}-svls-8583`, { + env, + }), ] // Tag all stacks so we can easily clean them up diff --git a/integration-tests/lambda/svls-8583-node/index.js b/integration-tests/lambda/svls-8583-node/index.js new file mode 100644 index 000000000..4954f4e38 --- /dev/null +++ b/integration-tests/lambda/svls-8583-node/index.js @@ -0,0 +1,15 @@ +'use strict'; + +// Handler for SVLS-8583 integration tests. +// Returns { Status: process.env.RETURN_STATUS } so the test can configure each function +// to return a specific durable execution status. +// When the Lambda event contains a DurableExecutionArn, the datadog-lambda-js wrapper +// reads result.Status and sets aws_lambda.durable_function.execution_status on the span. +exports.handler = async (event, context) => { + const returnStatus = process.env.RETURN_STATUS || 'SUCCEEDED'; + console.log(`Durable function handler: returning Status=${returnStatus}`); + return { + Status: returnStatus, + statusCode: 200, + }; +}; diff --git a/integration-tests/lambda/svls-8583-node/package.json b/integration-tests/lambda/svls-8583-node/package.json new file mode 100644 index 000000000..9930663b5 --- /dev/null +++ b/integration-tests/lambda/svls-8583-node/package.json @@ -0,0 +1,6 @@ +{ + "name": "svls-8583-test-lambda", + "version": "1.0.0", + "description": "Lambda handler for SVLS-8583 durable function execution status integration tests", + "main": "index.js" +} diff --git a/integration-tests/lib/stacks/svls-8583.ts b/integration-tests/lib/stacks/svls-8583.ts new file mode 100644 index 000000000..54c8396fc --- /dev/null +++ b/integration-tests/lib/stacks/svls-8583.ts @@ -0,0 +1,94 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; +import { + createLogGroup, + defaultDatadogEnvVariables, + defaultDatadogSecretPolicy, + getExtensionLayer, + getDefaultNodeLayer, + defaultNodeRuntime, +} from '../util'; + +// Stack for SVLS-8583: durable function execution status tag on the aws.lambda span. +// Three Node.js functions: +// - durable-succeeded: invoked with DurableExecutionArn event, returns Status=SUCCEEDED +// - durable-failed: invoked with DurableExecutionArn event, returns Status=FAILED +// - non-durable: invoked without DurableExecutionArn; tag must NOT be set +export class Svls8583Stack extends cdk.Stack { + constructor(scope: Construct, id: string, props: cdk.StackProps) { + super(scope, id, props); + + const extensionLayer = getExtensionLayer(this); + const nodeLayer = getDefaultNodeLayer(this); + + const commonNodeEnv = { + ...defaultDatadogEnvVariables, + DD_TRACE_ENABLED: 'true', + DD_LAMBDA_HANDLER: 'index.handler', + }; + + // --- durable-succeeded --- + const succeededName = `${id}-durable-succeeded`; + const succeededFn = new lambda.Function(this, succeededName, { + runtime: defaultNodeRuntime, + architecture: lambda.Architecture.ARM_64, + handler: '/opt/nodejs/node_modules/datadog-lambda-js/handler.handler', + code: lambda.Code.fromAsset('./lambda/svls-8583-node'), + functionName: succeededName, + timeout: cdk.Duration.seconds(30), + memorySize: 256, + environment: { + ...commonNodeEnv, + DD_SERVICE: succeededName, + RETURN_STATUS: 'SUCCEEDED', + }, + logGroup: createLogGroup(this, succeededName), + }); + succeededFn.addToRolePolicy(defaultDatadogSecretPolicy); + succeededFn.addLayers(extensionLayer); + succeededFn.addLayers(nodeLayer); + + // --- durable-failed --- + const failedName = `${id}-durable-failed`; + const failedFn = new lambda.Function(this, failedName, { + runtime: defaultNodeRuntime, + architecture: lambda.Architecture.ARM_64, + handler: '/opt/nodejs/node_modules/datadog-lambda-js/handler.handler', + code: lambda.Code.fromAsset('./lambda/svls-8583-node'), + functionName: failedName, + timeout: cdk.Duration.seconds(30), + memorySize: 256, + environment: { + ...commonNodeEnv, + DD_SERVICE: failedName, + RETURN_STATUS: 'FAILED', + }, + logGroup: createLogGroup(this, failedName), + }); + failedFn.addToRolePolicy(defaultDatadogSecretPolicy); + failedFn.addLayers(extensionLayer); + failedFn.addLayers(nodeLayer); + + // --- non-durable (guard: no DurableExecutionArn in event) --- + const nonDurableName = `${id}-non-durable`; + const nonDurableFn = new lambda.Function(this, nonDurableName, { + runtime: defaultNodeRuntime, + architecture: lambda.Architecture.ARM_64, + handler: '/opt/nodejs/node_modules/datadog-lambda-js/handler.handler', + code: lambda.Code.fromAsset('./lambda/svls-8583-node'), + functionName: nonDurableName, + timeout: cdk.Duration.seconds(30), + memorySize: 256, + environment: { + ...commonNodeEnv, + DD_SERVICE: nonDurableName, + RETURN_STATUS: 'SUCCEEDED', + }, + logGroup: createLogGroup(this, nonDurableName), + }); + nonDurableFn.addToRolePolicy(defaultDatadogSecretPolicy); + nonDurableFn.addLayers(extensionLayer); + nonDurableFn.addLayers(nodeLayer); + } +} diff --git a/integration-tests/tests/svls-8583.test.ts b/integration-tests/tests/svls-8583.test.ts new file mode 100644 index 000000000..482300d4d --- /dev/null +++ b/integration-tests/tests/svls-8583.test.ts @@ -0,0 +1,153 @@ +// SVLS-8583: [Tracer-JS] Add basic execution status to aws.lambda span +// Verifies that datadog-lambda-js sets the tag +// `aws_lambda.durable_function.execution_status` on the aws.lambda span when: +// 1. The Lambda event contains a DurableExecutionArn key +// 2. The Lambda result contains a `Status` field with a recognized value +// Also verifies the tag is NOT set for non-durable invocations (guard test). + +import { invokeLambda } from './utils/lambda'; +import { + getInvocationTracesLogsByRequestId, + InvocationTracesLogs, +} from './utils/datadog'; +import { getIdentifier } from '../config'; +import { DEFAULT_DATADOG_INDEXING_WAIT_MS } from '../config'; + +const identifier = getIdentifier(); +const stackName = `integ-${identifier}-svls-8583`; + +// A well-formed DurableExecutionArn (the JS guard only checks typeof === 'string', +// but use a realistic ARN for clarity). +const DURABLE_EVENT = { + DurableExecutionArn: + 'arn:aws:lambda:us-east-1:425362996713:function:test-func:1/durable-execution/my-workflow/exec-12345', +}; + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +describe('SVLS-8583: Durable Function Execution Status Tag', () => { + let succeededInvocStatusCode: number | undefined; + let failedInvocStatusCode: number | undefined; + let nonDurableInvocStatusCode: number | undefined; + let succeededResult: InvocationTracesLogs; + let failedResult: InvocationTracesLogs; + let nonDurableResult: InvocationTracesLogs; + + beforeAll(async () => { + const succeededFunctionName = `${stackName}-durable-succeeded`; + const failedFunctionName = `${stackName}-durable-failed`; + const nonDurableFunctionName = `${stackName}-non-durable`; + + // Invoke all three functions concurrently + const [succeededInvoc, failedInvoc, nonDurableInvoc] = await Promise.all([ + invokeLambda(succeededFunctionName, DURABLE_EVENT), + invokeLambda(failedFunctionName, DURABLE_EVENT), + invokeLambda(nonDurableFunctionName, {}), + ]); + + // Capture invocation status codes for assertions + succeededInvocStatusCode = succeededInvoc.statusCode; + failedInvocStatusCode = failedInvoc.statusCode; + nonDurableInvocStatusCode = nonDurableInvoc.statusCode; + + console.log(`Invoked ${succeededFunctionName}: requestId=${succeededInvoc.requestId}`); + console.log(`Invoked ${failedFunctionName}: requestId=${failedInvoc.requestId}`); + console.log(`Invoked ${nonDurableFunctionName}: requestId=${nonDurableInvoc.requestId}`); + + // Wait for Datadog to index traces + console.log(`Waiting ${DEFAULT_DATADOG_INDEXING_WAIT_MS / 1000}s for Datadog indexing...`); + await sleep(DEFAULT_DATADOG_INDEXING_WAIT_MS); + + // Collect telemetry for each invocation + [succeededResult, failedResult, nonDurableResult] = await Promise.all([ + getInvocationTracesLogsByRequestId(succeededFunctionName, succeededInvoc.requestId), + getInvocationTracesLogsByRequestId(failedFunctionName, failedInvoc.requestId), + getInvocationTracesLogsByRequestId(nonDurableFunctionName, nonDurableInvoc.requestId), + ]); + + console.log('All telemetry collected'); + }, 600000); + + describe('durable-succeeded: invoked with DurableExecutionArn, returns Status=SUCCEEDED', () => { + it('should invoke Lambda successfully', () => { + expect(succeededInvocStatusCode).toBe(200); + }); + + it('should send exactly one trace to Datadog', () => { + expect(succeededResult.traces?.length).toBe(1); + }); + + it('should have aws.lambda span with execution_status=SUCCEEDED', () => { + const trace = succeededResult.traces![0]; + const awsLambdaSpan = trace.spans.find( + (span: any) => span.attributes.operation_name === 'aws.lambda' + ); + expect(awsLambdaSpan).toBeDefined(); + expect(awsLambdaSpan).toMatchObject({ + attributes: { + operation_name: 'aws.lambda', + custom: { + aws_lambda: { + durable_function: { + execution_status: 'SUCCEEDED', + }, + }, + }, + }, + }); + }); + }); + + describe('durable-failed: invoked with DurableExecutionArn, returns Status=FAILED', () => { + it('should invoke Lambda successfully', () => { + expect(failedInvocStatusCode).toBe(200); + }); + + it('should send exactly one trace to Datadog', () => { + expect(failedResult.traces?.length).toBe(1); + }); + + it('should have aws.lambda span with execution_status=FAILED', () => { + const trace = failedResult.traces![0]; + const awsLambdaSpan = trace.spans.find( + (span: any) => span.attributes.operation_name === 'aws.lambda' + ); + expect(awsLambdaSpan).toBeDefined(); + expect(awsLambdaSpan).toMatchObject({ + attributes: { + operation_name: 'aws.lambda', + custom: { + aws_lambda: { + durable_function: { + execution_status: 'FAILED', + }, + }, + }, + }, + }); + }); + }); + + describe('non-durable: invoked without DurableExecutionArn (guard test)', () => { + it('should invoke Lambda successfully', () => { + expect(nonDurableInvocStatusCode).toBe(200); + }); + + it('should send exactly one trace to Datadog', () => { + expect(nonDurableResult.traces?.length).toBe(1); + }); + + it('should NOT have aws_lambda.durable_function.execution_status tag on aws.lambda span', () => { + const trace = nonDurableResult.traces![0]; + const awsLambdaSpan = trace.spans.find( + (span: any) => span.attributes.operation_name === 'aws.lambda' + ); + expect(awsLambdaSpan).toBeDefined(); + const executionStatus = + awsLambdaSpan?.attributes?.custom?.aws_lambda?.durable_function?.execution_status; + expect(executionStatus).toBeUndefined(); + }); + }); +});