Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions integration-tests/bin/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions integration-tests/lambda/svls-8583-node/index.js
Original file line number Diff line number Diff line change
@@ -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,
};
};
6 changes: 6 additions & 0 deletions integration-tests/lambda/svls-8583-node/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
94 changes: 94 additions & 0 deletions integration-tests/lib/stacks/svls-8583.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
153 changes: 153 additions & 0 deletions integration-tests/tests/svls-8583.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
});
});
});
Loading