Skip to content

Commit 837d598

Browse files
jchrostek-ddclaude
andauthored
test(integration): add enhanced duration metrics tests (#1107)
## Summary - Add integration tests for Lambda enhanced duration metrics (runtime_duration, billed_duration, duration, post_runtime_duration, init_duration) - Plan on following up with (1) more metrics (2) metrics on other runtimes like LMI. - No changes to Extension code. ## Test plan - [x] New integ tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4864422 commit 837d598

File tree

6 files changed

+157
-62
lines changed

6 files changed

+157
-62
lines changed

integration-tests/tests/lmi.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const identifier = getIdentifier();
99
const stackName = `integ-${identifier}-lmi`;
1010

1111
describe('LMI Integration Tests', () => {
12-
let results: Record<string, DatadogTelemetry[][]>;
12+
let telemetry: Record<string, DatadogTelemetry>;
1313

1414
beforeAll(async () => {
1515
const functions: FunctionConfig[] = runtimes.map(runtime => ({
@@ -20,13 +20,13 @@ describe('LMI Integration Tests', () => {
2020
console.log('Invoking LMI functions...');
2121

2222
// Invoke all LMI functions and collect telemetry
23-
results = await invokeAndCollectTelemetry(functions, 1);
23+
telemetry = await invokeAndCollectTelemetry(functions, 1);
2424

2525
console.log('LMI invocation and data fetching completed');
2626
}, 600000);
2727

2828
describe.each(runtimes)('%s Runtime with LMI', (runtime) => {
29-
const getResult = () => results[runtime]?.[0]?.[0];
29+
const getResult = () => telemetry[runtime]?.threads[0]?.[0];
3030

3131
it('should invoke Lambda successfully', () => {
3232
const result = getResult();

integration-tests/tests/on-demand.test.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invokeAndCollectTelemetry, FunctionConfig } from './utils/default';
2-
import { DatadogTelemetry } from './utils/datadog';
2+
import { DatadogTelemetry, DURATION_METRICS } from './utils/datadog';
33
import { forceColdStart } from './utils/lambda';
44
import { getIdentifier } from '../config';
55

@@ -10,27 +10,25 @@ const identifier = getIdentifier();
1010
const stackName = `integ-${identifier}-on-demand`;
1111

1212
describe('On-Demand Integration Tests', () => {
13-
let results: Record<string, DatadogTelemetry[][]>;
13+
let telemetry: Record<string, DatadogTelemetry>;
1414

1515
beforeAll(async () => {
1616
const functions: FunctionConfig[] = runtimes.map(runtime => ({
1717
functionName: `${stackName}-${runtime}-lambda`,
1818
runtime,
1919
}));
2020

21-
// Force cold starts
2221
await Promise.all(functions.map(fn => forceColdStart(fn.functionName)));
2322

24-
// Add 5s delay between invocations to ensure warm container is reused
25-
// Required because there is post-runtime processing with 'end' flush strategy
26-
results = await invokeAndCollectTelemetry(functions, 2, 1, 5000);
23+
telemetry = await invokeAndCollectTelemetry(functions, 2, 1, 5000);
2724

2825
console.log('All invocations and data fetching completed');
2926
}, 600000);
3027

3128
describe.each(runtimes)('%s runtime', (runtime) => {
32-
const getFirstInvocation = () => results[runtime]?.[0]?.[0];
33-
const getSecondInvocation = () => results[runtime]?.[0]?.[1];
29+
const getTelemetry = () => telemetry[runtime];
30+
const getFirstInvocation = () => getTelemetry()?.threads[0]?.[0];
31+
const getSecondInvocation = () => getTelemetry()?.threads[0]?.[1];
3432

3533
describe('first invocation (cold start)', () => {
3634
it('should invoke Lambda successfully', () => {
@@ -74,7 +72,6 @@ describe('On-Demand Integration Tests', () => {
7472
});
7573
});
7674

77-
// Python has known issues with cold_start spans - mark as failing
7875
if (runtime === 'python') {
7976
it.failing('[failing] should have aws.lambda.cold_start span', () => {
8077
const result = getFirstInvocation();
@@ -151,5 +148,13 @@ describe('On-Demand Integration Tests', () => {
151148
expect(coldStartSpan).toBeUndefined();
152149
});
153150
});
151+
152+
describe.skip.each(DURATION_METRICS)('%s', (metric) => {
153+
it('should have points with positive values', () => {
154+
const points = getTelemetry().metrics[metric];
155+
expect(points.length).toBeGreaterThan(0);
156+
expect(points.every(p => p.value >= 0)).toBe(true);
157+
});
158+
});
154159
});
155160
});

integration-tests/tests/otlp.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const identifier = getIdentifier();
99
const stackName = `integ-${identifier}-otlp`;
1010

1111
describe('OTLP Integration Tests', () => {
12-
let results: Record<string, DatadogTelemetry[][]>;
12+
let telemetry: Record<string, DatadogTelemetry>;
1313

1414
beforeAll(async () => {
1515
// Build function configs for all runtimes plus response validation
@@ -27,13 +27,13 @@ describe('OTLP Integration Tests', () => {
2727
console.log('Invoking all OTLP Lambda functions...');
2828

2929
// Invoke all OTLP functions and collect telemetry
30-
results = await invokeAndCollectTelemetry(functions, 1, 1, 0, {}, DATADOG_INDEXING_WAIT_5_MIN_MS);
30+
telemetry = await invokeAndCollectTelemetry(functions, 1, 1, 0, {}, DATADOG_INDEXING_WAIT_5_MIN_MS);
3131

3232
console.log('All OTLP Lambda invocations and data fetching completed');
3333
}, 700000);
3434

3535
describe.each(runtimes)('%s Runtime', (runtime) => {
36-
const getResult = () => results[runtime]?.[0]?.[0];
36+
const getResult = () => telemetry[runtime]?.threads[0]?.[0];
3737

3838
it('should invoke Lambda successfully', () => {
3939
const result = getResult();
@@ -56,7 +56,7 @@ describe('OTLP Integration Tests', () => {
5656
});
5757

5858
describe('OTLP Response Validation', () => {
59-
const getResult = () => results['responseValidation']?.[0]?.[0];
59+
const getResult = () => telemetry['responseValidation']?.threads[0]?.[0];
6060

6161
it('should invoke response validation Lambda successfully', () => {
6262
const result = getResult();

integration-tests/tests/snapstart.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const identifier = getIdentifier();
1010
const stackName = `integ-${identifier}-snapstart`;
1111

1212
describe('Snapstart Integration Tests', () => {
13-
let results: Record<string, DatadogTelemetry[][]>;
13+
let telemetry: Record<string, DatadogTelemetry>;
1414

1515
beforeAll(async () => {
1616
// Publish new versions and wait for SnapStart optimization
@@ -43,20 +43,20 @@ describe('Snapstart Integration Tests', () => {
4343
// - Second invocation: warm (no snapstart_restore span)
4444
// - 5s delay ensures warm container reuse
4545
// - 2 threads for trace isolation testing
46-
results = await invokeAndCollectTelemetry(functions, 2, 2, 5000);
46+
telemetry = await invokeAndCollectTelemetry(functions, 2, 2, 5000);
4747

4848
console.log('All Snapstart Lambda invocations and data fetching completed');
4949
}, 900000);
5050

5151
describe.each(runtimes)('%s Runtime with SnapStart', (runtime) => {
5252
// With concurrency=2, invocations=2:
53-
// - results[runtime][0][0] = thread 0, first invocation (restore)
54-
// - results[runtime][0][1] = thread 0, second invocation (warm)
55-
// - results[runtime][1][0] = thread 1, first invocation (restore)
56-
// - results[runtime][1][1] = thread 1, second invocation (warm)
57-
const getRestoreInvocation = () => results[runtime]?.[0]?.[0];
58-
const getWarmInvocation = () => results[runtime]?.[0]?.[1];
59-
const getOtherThreadInvocation = () => results[runtime]?.[1]?.[0];
53+
// - telemetry[runtime].threads[0][0] = thread 0, first invocation (restore)
54+
// - telemetry[runtime].threads[0][1] = thread 0, second invocation (warm)
55+
// - telemetry[runtime].threads[1][0] = thread 1, first invocation (restore)
56+
// - telemetry[runtime].threads[1][1] = thread 1, second invocation (warm)
57+
const getRestoreInvocation = () => telemetry[runtime]?.threads[0]?.[0];
58+
const getWarmInvocation = () => telemetry[runtime]?.threads[0]?.[1];
59+
const getOtherThreadInvocation = () => telemetry[runtime]?.threads[1]?.[0];
6060

6161
describe('first invocation (restore from snapshot)', () => {
6262
it('should invoke successfully', () => {
@@ -150,10 +150,10 @@ describe('Snapstart Integration Tests', () => {
150150

151151
describe('trace isolation', () => {
152152
it('should have different trace IDs for all 4 invocations', () => {
153-
const thread0Restore = results[runtime]?.[0]?.[0];
154-
const thread0Warm = results[runtime]?.[0]?.[1];
155-
const thread1Restore = results[runtime]?.[1]?.[0];
156-
const thread1Warm = results[runtime]?.[1]?.[1];
153+
const thread0Restore = telemetry[runtime]?.threads[0]?.[0];
154+
const thread0Warm = telemetry[runtime]?.threads[0]?.[1];
155+
const thread1Restore = telemetry[runtime]?.threads[1]?.[0];
156+
const thread1Warm = telemetry[runtime]?.threads[1]?.[1];
157157

158158
expect(thread0Restore).toBeDefined();
159159
expect(thread0Warm).toBeDefined();

integration-tests/tests/utils/datadog.ts

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ function formatDatadogError(error: unknown, query: string): string {
5555
}
5656

5757
export interface DatadogTelemetry {
58+
threads: InvocationTracesLogs[][]; // [thread][invocation]
59+
metrics: EnhancedMetrics;
60+
}
61+
62+
export interface InvocationTracesLogs {
5863
requestId: string;
5964
statusCode?: number;
6065
traces?: DatadogTrace[];
@@ -78,6 +83,21 @@ export interface DatadogLog {
7883
tags: string[];
7984
}
8085

86+
export const DURATION_METRICS = [
87+
'aws.lambda.enhanced.runtime_duration',
88+
'aws.lambda.enhanced.billed_duration',
89+
'aws.lambda.enhanced.duration',
90+
'aws.lambda.enhanced.post_runtime_duration',
91+
'aws.lambda.enhanced.init_duration',
92+
];
93+
94+
export type EnhancedMetrics = Record<string, MetricPoint[]>;
95+
96+
export interface MetricPoint {
97+
timestamp: number;
98+
value: number;
99+
}
100+
81101
/**
82102
* Extracts the base service name from a function name by stripping any
83103
* version qualifier (:N) or alias qualifier (:alias)
@@ -90,7 +110,7 @@ function getServiceName(functionName: string): string {
90110
return functionName.substring(0, colonIndex);
91111
}
92112

93-
export async function getDatadogTelemetryByRequestId(functionName: string, requestId: string): Promise<DatadogTelemetry> {
113+
export async function getInvocationTracesLogsByRequestId(functionName: string, requestId: string): Promise<InvocationTracesLogs> {
94114
const serviceName = getServiceName(functionName);
95115
const traces = await getTraces(serviceName, requestId);
96116
const logs = await getLogs(serviceName, requestId);
@@ -107,16 +127,14 @@ export async function getTraces(
107127
requestId: string,
108128
): Promise<DatadogTrace[]> {
109129
const now = Date.now();
110-
const fromTime = now - (1 * 60 * 60 * 1000); // 1 hour ago
130+
const fromTime = now - (1 * 60 * 60 * 1000);
111131
const toTime = now;
112-
// Convert service name to lowercase as Datadog stores it that way
113132
const serviceNameLower = serviceName.toLowerCase();
114133
const query = `service:${serviceNameLower} @request_id:${requestId}`;
115134

116135
try {
117136
console.log(`Searching for traces: ${query}`);
118137

119-
// First, find spans matching the request_id to get trace IDs
120138
const initialResponse = await datadogClient.post('/api/v2/spans/events/search', {
121139
data: {
122140
type: 'search_request',
@@ -137,7 +155,6 @@ export async function getTraces(
137155
const initialSpans = initialResponse.data.data || [];
138156
console.log(`Found ${initialSpans.length} initial span(s)`);
139157

140-
// Extract unique trace IDs
141158
const traceIds = new Set<string>();
142159
for (const spanData of initialSpans) {
143160
const traceId = spanData.attributes?.trace_id;
@@ -148,7 +165,6 @@ export async function getTraces(
148165

149166
console.log(`Found ${traceIds.size} unique trace(s)`);
150167

151-
// Now fetch all spans for each trace ID
152168
const allSpans: any[] = [];
153169
for (const traceId of traceIds) {
154170
const traceResponse = await datadogClient.post('/api/v2/spans/events/search', {
@@ -171,7 +187,6 @@ export async function getTraces(
171187
allSpans.push(...traceSpans);
172188
}
173189

174-
// Group spans by trace_id to reconstruct traces
175190
const traceMap = new Map<string, DatadogSpan[]>();
176191

177192
for (const spanData of allSpans) {
@@ -190,7 +205,6 @@ export async function getTraces(
190205
}
191206
}
192207

193-
// Convert map to array of traces
194208
const traces: DatadogTrace[] = [];
195209
for (const [traceId, spans] of traceMap.entries()) {
196210
traces.push({
@@ -216,7 +230,7 @@ export async function getLogs(
216230
requestId: string,
217231
): Promise<DatadogLog[]> {
218232
const now = Date.now();
219-
const fromTime = now - (2 * 60 * 60 * 1000); // 2 hours ago
233+
const fromTime = now - (2 * 60 * 60 * 1000);
220234
const toTime = now;
221235
const query = `service:${serviceName} @lambda.request_id:${requestId}`;
222236

@@ -237,7 +251,6 @@ export async function getLogs(
237251
const rawLogs = response.data.data || [];
238252
console.log(`Found ${rawLogs.length} log(s)`);
239253

240-
// Transform raw logs to DatadogLog format
241254
const logs: DatadogLog[] = rawLogs.map((logData: any) => {
242255
const attrs = logData.attributes || {};
243256
return {
@@ -255,3 +268,55 @@ export async function getLogs(
255268
throw error;
256269
}
257270
}
271+
272+
export async function getEnhancedMetrics(
273+
functionName: string,
274+
fromTime: number,
275+
toTime: number
276+
): Promise<EnhancedMetrics> {
277+
const promises = DURATION_METRICS.map(async (metricName) => {
278+
const points = await getMetrics(metricName, functionName, fromTime, toTime);
279+
return { metricName, points };
280+
});
281+
282+
const results = await Promise.all(promises);
283+
284+
const metrics: EnhancedMetrics = {};
285+
for (const { metricName, points } of results) {
286+
metrics[metricName] = points;
287+
}
288+
289+
return metrics;
290+
}
291+
292+
async function getMetrics(
293+
metricName: string,
294+
functionName: string,
295+
fromTime: number,
296+
toTime: number
297+
): Promise<MetricPoint[]> {
298+
const baseFunctionName = getServiceName(functionName).toLowerCase();
299+
const query = `avg:${metricName}{functionname:${baseFunctionName}}`;
300+
301+
console.log(`Querying metrics: ${query}`);
302+
303+
const response = await datadogClient.get('/api/v1/query', {
304+
params: {
305+
query,
306+
from: Math.floor(fromTime / 1000),
307+
to: Math.floor(toTime / 1000),
308+
},
309+
});
310+
311+
const series = response.data.series || [];
312+
console.log(`Found ${series.length} series for ${metricName}`);
313+
314+
if (series.length === 0) {
315+
return [];
316+
}
317+
318+
return (series[0].pointlist || []).map((p: [number, number]) => ({
319+
timestamp: p[0],
320+
value: p[1],
321+
}));
322+
}

0 commit comments

Comments
 (0)