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
44 changes: 41 additions & 3 deletions integration-tests/tests/lmi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const identifier = getIdentifier();
const stackName = `integ-${identifier}-lmi`;

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

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

// Invoke all LMI functions and collect telemetry
results = await invokeAndCollectTelemetry(functions, 1);
telemetry = await invokeAndCollectTelemetry(functions, 1);

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

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

it('should invoke Lambda successfully', () => {
const result = getResult();
Expand Down Expand Up @@ -110,5 +110,43 @@ describe('LMI Integration Tests', () => {
expect(awsLambdaSpan?.attributes.custom.cold_start).toBeUndefined();
}
});

// All duration metrics tests are skipped - metrics indexing is unreliable
// TODO: Investigate why Datadog metrics API returns inconsistent results
describe.skip('duration metrics', () => {
it('should emit aws.lambda.enhanced.runtime_duration', () => {
const points = telemetry[runtime].metrics.duration['runtime_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('should emit aws.lambda.enhanced.billed_duration', () => {
const points = telemetry[runtime].metrics.duration['billed_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('should emit aws.lambda.enhanced.duration', () => {
const points = telemetry[runtime].metrics.duration['duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('should emit aws.lambda.enhanced.post_runtime_duration', () => {
const points = telemetry[runtime].metrics.duration['post_runtime_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThanOrEqual(0);
});

it('duration should be >= runtime_duration', () => {
const durationPoints = telemetry[runtime].metrics.duration['duration'];
const runtimePoints = telemetry[runtime].metrics.duration['runtime_duration'];
expect(durationPoints.length).toBeGreaterThan(0);
expect(runtimePoints.length).toBeGreaterThan(0);
const duration = durationPoints[durationPoints.length - 1].value;
const runtimeDuration = runtimePoints[runtimePoints.length - 1].value;
expect(duration).toBeGreaterThanOrEqual(runtimeDuration);
});
});
});
});
55 changes: 51 additions & 4 deletions integration-tests/tests/on-demand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const identifier = getIdentifier();
const stackName = `integ-${identifier}-on-demand`;

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

beforeAll(async () => {
const functions: FunctionConfig[] = runtimes.map(runtime => ({
Expand All @@ -23,14 +23,16 @@ describe('On-Demand Integration Tests', () => {

// Add 5s delay between invocations to ensure warm container is reused
// Required because there is post-runtime processing with 'end' flush strategy
results = await invokeAndCollectTelemetry(functions, 2, 1, 5000);
// invokeAndCollectTelemetry now returns DatadogTelemetry with metrics included
telemetry = await invokeAndCollectTelemetry(functions, 2, 1, 5000);

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

describe.each(runtimes)('%s runtime', (runtime) => {
const getFirstInvocation = () => results[runtime]?.[0]?.[0];
const getSecondInvocation = () => results[runtime]?.[0]?.[1];
const getTelemetry = () => telemetry[runtime];
const getFirstInvocation = () => getTelemetry()?.threads[0]?.[0];
const getSecondInvocation = () => getTelemetry()?.threads[0]?.[1];

describe('first invocation (cold start)', () => {
it('should invoke Lambda successfully', () => {
Expand Down Expand Up @@ -151,5 +153,50 @@ describe('On-Demand Integration Tests', () => {
expect(coldStartSpan).toBeUndefined();
});
});

// All duration metrics tests are skipped - metrics indexing is unreliable
// TODO: Investigate why Datadog metrics API returns inconsistent results
describe.skip('duration metrics', () => {
it('should emit aws.lambda.enhanced.runtime_duration', () => {
const points = getTelemetry().metrics.duration['runtime_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('should emit aws.lambda.enhanced.billed_duration', () => {
const points = getTelemetry().metrics.duration['billed_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('should emit aws.lambda.enhanced.duration', () => {
const points = getTelemetry().metrics.duration['duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('should emit aws.lambda.enhanced.post_runtime_duration', () => {
const points = getTelemetry().metrics.duration['post_runtime_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThanOrEqual(0);
});

// First invocation is a forced cold start, so init_duration should be emitted
it('should emit aws.lambda.enhanced.init_duration for cold start', () => {
const points = getTelemetry().metrics.duration['init_duration'];
expect(points.length).toBeGreaterThan(0);
expect(points[points.length - 1].value).toBeGreaterThan(0);
});

it('duration should be >= runtime_duration', () => {
const durationPoints = getTelemetry().metrics.duration['duration'];
const runtimePoints = getTelemetry().metrics.duration['runtime_duration'];
expect(durationPoints.length).toBeGreaterThan(0);
expect(runtimePoints.length).toBeGreaterThan(0);
const duration = durationPoints[durationPoints.length - 1].value;
const runtimeDuration = runtimePoints[runtimePoints.length - 1].value;
expect(duration).toBeGreaterThanOrEqual(runtimeDuration);
});
});
});
});
8 changes: 4 additions & 4 deletions integration-tests/tests/otlp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const identifier = getIdentifier();
const stackName = `integ-${identifier}-otlp`;

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

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

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

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

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

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

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

it('should invoke response validation Lambda successfully', () => {
const result = getResult();
Expand Down
26 changes: 13 additions & 13 deletions integration-tests/tests/snapstart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const identifier = getIdentifier();
const stackName = `integ-${identifier}-snapstart`;

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

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

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

describe.each(runtimes)('%s Runtime with SnapStart', (runtime) => {
// With concurrency=2, invocations=2:
// - results[runtime][0][0] = thread 0, first invocation (restore)
// - results[runtime][0][1] = thread 0, second invocation (warm)
// - results[runtime][1][0] = thread 1, first invocation (restore)
// - results[runtime][1][1] = thread 1, second invocation (warm)
const getRestoreInvocation = () => results[runtime]?.[0]?.[0];
const getWarmInvocation = () => results[runtime]?.[0]?.[1];
const getOtherThreadInvocation = () => results[runtime]?.[1]?.[0];
// - telemetry[runtime].threads[0][0] = thread 0, first invocation (restore)
// - telemetry[runtime].threads[0][1] = thread 0, second invocation (warm)
// - telemetry[runtime].threads[1][0] = thread 1, first invocation (restore)
// - telemetry[runtime].threads[1][1] = thread 1, second invocation (warm)
const getRestoreInvocation = () => telemetry[runtime]?.threads[0]?.[0];
const getWarmInvocation = () => telemetry[runtime]?.threads[0]?.[1];
const getOtherThreadInvocation = () => telemetry[runtime]?.threads[1]?.[0];

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

describe('trace isolation', () => {
it('should have different trace IDs for all 4 invocations', () => {
const thread0Restore = results[runtime]?.[0]?.[0];
const thread0Warm = results[runtime]?.[0]?.[1];
const thread1Restore = results[runtime]?.[1]?.[0];
const thread1Warm = results[runtime]?.[1]?.[1];
const thread0Restore = telemetry[runtime]?.threads[0]?.[0];
const thread0Warm = telemetry[runtime]?.threads[0]?.[1];
const thread1Restore = telemetry[runtime]?.threads[1]?.[0];
const thread1Warm = telemetry[runtime]?.threads[1]?.[1];

expect(thread0Restore).toBeDefined();
expect(thread0Warm).toBeDefined();
Expand Down
124 changes: 123 additions & 1 deletion integration-tests/tests/utils/datadog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const datadogClient: AxiosInstance = axios.create({
});

export interface DatadogTelemetry {
threads: InvocationTracesLogs[][]; // [thread][invocation]
metrics: EnhancedMetrics;
}

export interface InvocationTracesLogs {
requestId: string;
statusCode?: number;
traces?: DatadogTrace[];
Expand All @@ -41,6 +46,27 @@ export interface DatadogLog {
tags: string[];
}

export const ENHANCED_METRICS_CONFIG = {
duration: [
'aws.lambda.enhanced.runtime_duration',
'aws.lambda.enhanced.billed_duration',
'aws.lambda.enhanced.duration',
'aws.lambda.enhanced.post_runtime_duration',
'aws.lambda.enhanced.init_duration',
],
} as const;

export type MetricCategory = keyof typeof ENHANCED_METRICS_CONFIG;

export type EnhancedMetrics = {
[K in MetricCategory]: Record<string, MetricPoint[]>;
};

export interface MetricPoint {
timestamp: number;
value: number;
}

/**
* Extracts the base service name from a function name by stripping any
* version qualifier (:N) or alias qualifier (:alias)
Expand All @@ -53,7 +79,7 @@ function getServiceName(functionName: string): string {
return functionName.substring(0, colonIndex);
}

export async function getDatadogTelemetryByRequestId(functionName: string, requestId: string): Promise<DatadogTelemetry> {
export async function getInvocationTracesLogsByRequestId(functionName: string, requestId: string): Promise<InvocationTracesLogs> {
const serviceName = getServiceName(functionName);
const traces = await getTraces(serviceName, requestId);
const logs = await getLogs(serviceName, requestId);
Expand Down Expand Up @@ -219,3 +245,99 @@ export async function getLogs(
throw error;
}
}

/**
* Fetch all enhanced metrics for a function based on config
*/
export async function getEnhancedMetrics(
functionName: string,
fromTime: number,
toTime: number
): Promise<EnhancedMetrics> {
const result: Partial<EnhancedMetrics> = {};

// Fetch all categories in parallel
const categoryPromises = Object.entries(ENHANCED_METRICS_CONFIG).map(
async ([category, metricNames]) => {
const categoryMetrics = await fetchMetricCategory(
metricNames as readonly string[],
functionName,
fromTime,
toTime
);
return { category, metrics: categoryMetrics };
}
);

const categoryResults = await Promise.all(categoryPromises);

for (const { category, metrics } of categoryResults) {
result[category as MetricCategory] = metrics;
}

return result as EnhancedMetrics;
}

/**
* Fetch all metrics in a category in parallel
*/
async function fetchMetricCategory(
metricNames: readonly string[],
functionName: string,
fromTime: number,
toTime: number
): Promise<Record<string, MetricPoint[]>> {
const promises = metricNames.map(async (metricName) => {
const points = await getMetrics(metricName, functionName, fromTime, toTime);
// Use short name (last part after the last dot)
const shortName = metricName.split('.').pop()!;
return { shortName, points };
});

const results = await Promise.all(promises);

const metrics: Record<string, MetricPoint[]> = {};
for (const { shortName, points } of results) {
metrics[shortName] = points;
}

return metrics;
}

/**
* Query Datadog Metrics API v1 for a specific metric.
* Requires the DD_API_KEY to have 'timeseries_query' scope.
*/
async function getMetrics(
metricName: string,
functionName: string,
fromTime: number,
toTime: number
): Promise<MetricPoint[]> {
// Strip alias/version from function name - metrics are tagged with base name only
const baseFunctionName = getServiceName(functionName).toLowerCase();
const query = `avg:${metricName}{functionname:${baseFunctionName}}`;

console.log(`Querying metrics: ${query}`);

const response = await datadogClient.get('/api/v1/query', {
params: {
query,
from: Math.floor(fromTime / 1000),
to: Math.floor(toTime / 1000),
},
});

const series = response.data.series || [];
console.log(`Found ${series.length} series for ${metricName}`);

if (series.length === 0) {
return [];
}

// Return points from first series
return (series[0].pointlist || []).map((p: [number, number]) => ({
timestamp: p[0],
value: p[1],
}));
}
Loading