Skip to content

test(tracer): streamline e2e test structure #3031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 9, 2024
Merged
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
51 changes: 1 addition & 50 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion packages/testing/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,13 @@ type XRayTraceDocumentParsed = {
request_id: string;
};
http?: {
response: {
request: {
url: string;
method: string;
};
response?: {
status: number;
content_length?: number;
};
};
origin?: string;
Expand Down Expand Up @@ -142,6 +147,7 @@ type XRayTraceDocumentParsed = {
message: string;
};
error?: boolean;
namespace?: string;
};

type XRaySegmentParsed = {
Expand All @@ -163,6 +169,10 @@ type GetXRayTraceDetailsOptions = {
* The expected number of segments in each trace
*/
expectedSegmentsCount: number;
/**
* The name of the function that the trace is expected to be associated with
*/
functionName: string;
};

/**
Expand Down
162 changes: 86 additions & 76 deletions packages/testing/src/xray-traces-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,66 @@ const retriableGetTraceIds = (options: GetXRayTraceIdsOptions) =>
endTime.getTime() / 1000
)} --filter-expression 'resource.arn ENDSWITH ":function:${options.resourceName}"'`
);

throw new Error(
`Failed to get trace IDs after ${retryOptions.retries} retries`,
{ cause: error }
);
}
retry(error);
}
});
}, retryOptions);

/**
* Find the main Powertools subsegment in the trace
*
* A main Powertools subsegment is identified by the `## index.` suffix. Depending on the
* runtime, it may also be identified by the `Invocation` name.
*
* @param trace - The trace to find the main Powertools subsegment
* @param functionName - The function name to find the main Powertools subsegment
*/
const findMainPowertoolsSubsegment = (
trace: XRayTraceDocumentParsed,
functionName: string
) => {
const maybePowertoolsSubsegment = trace.subsegments?.find(
(subsegment) =>
subsegment.name.startsWith('## index.') ||
subsegment.name === 'Invocation'
);

if (!maybePowertoolsSubsegment) {
throw new Error(`Main subsegment not found for ${functionName} segment`);
}

if (maybePowertoolsSubsegment.name === 'Invocation') {
const powertoolsSubsegment = maybePowertoolsSubsegment.subsegments?.find(
(subsegment) => subsegment.name.startsWith('## index.')
);

if (!powertoolsSubsegment) {
throw new Error(`Main subsegment not found for ${functionName} segment`);
}

return powertoolsSubsegment;
}

return maybePowertoolsSubsegment;
};

/**
* Parse and sort the trace segments by start time
*
* @param trace - The trace to parse and sort
* @param expectedSegmentsCount - The expected segments count for the trace
* @param functionName - The function name to find the main Powertools subsegment
*/
const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => {
const parseAndSortTrace = (
trace: Trace,
expectedSegmentsCount: number,
functionName: string
) => {
const { Id: id, Segments: segments } = trace;
if (segments === undefined || segments.length !== expectedSegmentsCount) {
throw new Error(
Expand All @@ -111,9 +159,14 @@ const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => {
);
}

const parsedDocument = JSON.parse(Document) as XRayTraceDocumentParsed;
if (parsedDocument.origin === 'AWS::Lambda::Function') {
findMainPowertoolsSubsegment(parsedDocument, functionName);
}

parsedSegments.push({
Id,
Document: JSON.parse(Document) as XRayTraceDocumentParsed,
Document: parsedDocument,
});
}

Expand All @@ -136,15 +189,14 @@ const parseAndSortTrace = (trace: Trace, expectedSegmentsCount: number) => {
const getTraceDetails = async (
options: GetXRayTraceDetailsOptions
): Promise<XRayTraceParsed[]> => {
const { traceIds, expectedSegmentsCount } = options;
const { traceIds, expectedSegmentsCount, functionName } = options;
const response = await xrayClient.send(
new BatchGetTracesCommand({
TraceIds: traceIds,
})
);

const traces = response.Traces;

const { Traces: traces } = response;
if (traces === undefined || traces.length !== traceIds.length) {
throw new Error(
`Expected ${traceIds.length} traces, got ${traces ? traces.length : 0}`
Expand All @@ -153,7 +205,9 @@ const getTraceDetails = async (

const parsedAndSortedTraces: XRayTraceParsed[] = [];
for (const trace of traces) {
parsedAndSortedTraces.push(parseAndSortTrace(trace, expectedSegmentsCount));
parsedAndSortedTraces.push(
parseAndSortTrace(trace, expectedSegmentsCount, functionName)
);
}

return parsedAndSortedTraces.sort(
Expand All @@ -168,16 +222,28 @@ const getTraceDetails = async (
* @param options - The options to get trace details, including the trace IDs and expected segments count
*/
const retriableGetTraceDetails = (options: GetXRayTraceDetailsOptions) =>
promiseRetry(async (retry) => {
promiseRetry(async (retry, attempt) => {
try {
return await getTraceDetails(options);
} catch (error) {
if (attempt === retryOptions.retries) {
console.log(
`Manual query: aws xray batch-get-traces --trace-ids ${
options.traceIds
}`
);

throw new Error(
`Failed to get trace details after ${retryOptions.retries} retries`,
{ cause: error }
);
}
retry(error);
}
});
}, retryOptions);

/**
* Find the main function segment in the trace identified by the `## index.` suffix
* Find the main function segment within the `AWS::Lambda::Function` segment
*/
const findPowertoolsFunctionSegment = (
trace: XRayTraceParsed,
Expand All @@ -194,30 +260,7 @@ const findPowertoolsFunctionSegment = (
}

const document = functionSegment.Document;

const maybePowertoolsSubsegment = document.subsegments?.find(
(subsegment) =>
subsegment.name.startsWith('## index.') ||
subsegment.name === 'Invocation'
);

if (!maybePowertoolsSubsegment) {
throw new Error(`Main subsegment not found for ${functionName} segment`);
}

if (maybePowertoolsSubsegment.name === 'Invocation') {
const powertoolsSubsegment = maybePowertoolsSubsegment.subsegments?.find(
(subsegment) => subsegment.name.startsWith('## index.')
);

if (!powertoolsSubsegment) {
throw new Error(`Main subsegment not found for ${functionName} segment`);
}

return powertoolsSubsegment;
}

return maybePowertoolsSubsegment;
return findMainPowertoolsSubsegment(document, functionName);
};

/**
Expand Down Expand Up @@ -271,6 +314,7 @@ const getXRayTraceData = async (
const traces = await retriableGetTraceDetails({
traceIds,
expectedSegmentsCount,
functionName: resourceName,
});

if (!traces) {
Expand All @@ -286,9 +330,15 @@ const getXRayTraceData = async (
* @param options - The options to get the X-Ray trace data, including the start time, resource name, expected traces count, and expected segments count
*/
const getTraces = async (
options: GetXRayTraceIdsOptions & Omit<GetXRayTraceDetailsOptions, 'traceIds'>
options: GetXRayTraceIdsOptions &
Omit<GetXRayTraceDetailsOptions, 'traceIds' | 'functionName'> & {
resourceName: string;
}
): Promise<EnrichedXRayTraceDocumentParsed[]> => {
const traces = await getXRayTraceData(options);
const traces = await getXRayTraceData({
...options,
functionName: options.resourceName,
});

const { resourceName } = options;

Expand All @@ -305,45 +355,6 @@ const getTraces = async (
return mainSubsegments;
};

/**
* Get the X-Ray trace data for a given resource name without the main subsegments.
*
* This is useful when we are testing cases where Active Tracing is disabled and we don't have the main subsegments.
*
* @param options - The options to get the X-Ray trace data, including the start time, resource name, expected traces count, and expected segments count
*/
const getTracesWithoutMainSubsegments = async (
options: GetXRayTraceIdsOptions & Omit<GetXRayTraceDetailsOptions, 'traceIds'>
): Promise<EnrichedXRayTraceDocumentParsed[]> => {
const traces = await getXRayTraceData(options);

const { resourceName } = options;

const lambdaFunctionSegments = [];
for (const trace of traces) {
const functionSegment = trace.Segments.find(
(segment) => segment.Document.origin === 'AWS::Lambda::Function'
);

if (!functionSegment) {
throw new Error(
`AWS::Lambda::Function segment not found for ${resourceName}`
);
}

const lambdaFunctionSegment = functionSegment.Document;
const enrichedSubsegment = {
...lambdaFunctionSegment,
subsegments: parseSubsegmentsByName(
lambdaFunctionSegment.subsegments ?? []
),
};
lambdaFunctionSegments.push(enrichedSubsegment);
}

return lambdaFunctionSegments;
};

export {
getTraceIds,
retriableGetTraceIds,
Expand All @@ -352,5 +363,4 @@ export {
findPowertoolsFunctionSegment,
getTraces,
parseSubsegmentsByName,
getTracesWithoutMainSubsegments,
};
3 changes: 1 addition & 2 deletions packages/tracer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
"devDependencies": {
"@aws-lambda-powertools/testing-utils": "file:../testing",
"@aws-sdk/client-dynamodb": "^3.637.0",
"@aws-sdk/client-xray": "^3.637.0",
"aws-sdk": "^2.1688.0"
"@aws-sdk/client-xray": "^3.637.0"
},
"peerDependencies": {
"@middy/core": "4.x || 5.x"
Expand Down
Loading