packages/core/lib/patchers/aws3_p.ts (171 lines of code) (raw):
import {
Pluggable,
BuildMiddleware,
MiddlewareStack,
BuildHandlerOptions,
} from '@aws-sdk/types';
import { RegionResolvedConfig } from '@smithy/config-resolver';
import { isThrottlingError } from '@smithy/service-error-classification';
import { SdkError } from '@smithy/smithy-client';
import ServiceSegment from '../segments/attributes/aws';
import { stringify } from 'querystring';
import Subsegment from '../segments/attributes/subsegment';
const contextUtils = require('../context_utils');
const logger = require('../logger');
const { safeParseInt } = require('../utils');
import { getCauseTypeFromHttpStatus } from '../utils';
import { SegmentLike } from '../aws-xray';
const XRAY_PLUGIN_NAME = 'XRaySDKInstrumentation';
interface HttpResponse {
response?: {
status?: number,
content_length?: number
}
}
const buildAttributesFromMetadata = async (
service: string,
operation: string,
region: string,
commandInput: any,
res: any | null,
error: SdkError | null,
): Promise<[ServiceSegment, HttpResponse]> => {
const { extendedRequestId, requestId, httpStatusCode: statusCode, attempts } = res?.output?.$metadata || error?.$metadata;
const aws = new ServiceSegment(
{
extendedRequestId,
requestId,
retryCount: attempts,
data: res?.output,
request: {
operation,
params: commandInput,
httpRequest: {
region,
statusCode,
},
},
},
service,
);
const http: HttpResponse = {};
if (statusCode) {
http.response = {};
http.response.status = statusCode;
}
if (res?.response?.headers && res?.response?.headers['content-length'] !== undefined) {
if (!http.response) {
http.response = {};
}
http.response.content_length = safeParseInt(res.response.headers['content-length']);
}
return [aws, http];
};
function addFlags(http: HttpResponse, subsegment: Subsegment, err?: SdkError): void {
if (err && isThrottlingError(err)) {
subsegment.addThrottleFlag();
} else if (safeParseInt(http.response?.status) === 429 || safeParseInt(err?.$metadata?.httpStatusCode) === 429) {
subsegment.addThrottleFlag();
}
const cause = getCauseTypeFromHttpStatus(safeParseInt(http.response?.status));
if (cause === 'fault') {
subsegment.addFaultFlag();
} else if (cause === 'error') {
subsegment.addErrorFlag();
}
}
const getXRayMiddleware = (config: RegionResolvedConfig, manualSegment?: SegmentLike): BuildMiddleware<any, any> => (next: any, context: any) => async (args: any) => {
const segment = contextUtils.isAutomaticMode() ? contextUtils.resolveSegment() : manualSegment;
const {clientName, commandName} = context;
const commandInput = args?.input ?? {};
const commandOperation: string = commandName.slice(0, -7); // Strip trailing "Command" string
const operation: string = commandOperation.charAt(0).toLowerCase() + commandOperation.slice(1);
const service: string = clientName.slice(0, -6); // Strip trailing "Client" string
if (!segment) {
const output = service + '.' + operation;
if (!contextUtils.isAutomaticMode()) {
logger.getLogger().info('Call ' + output + ' requires a segment object' +
' passed to captureAWSv3Client for tracing in manual mode. Ignoring.');
} else {
logger.getLogger().info('Call ' + output +
' is missing the sub/segment context for automatic mode. Ignoring.');
}
return next(args);
}
let subsegment: Subsegment;
if (segment.notTraced) {
subsegment = segment.addNewSubsegmentWithoutSampling(service);
} else {
subsegment = segment.addNewSubsegment(service);
}
subsegment.addAttribute('namespace', 'aws');
const parent = (segment instanceof Subsegment ? segment.segment : segment);
const data = parent.segment ? parent.segment.additionalTraceData : parent.additionalTraceData;
let traceHeader = stringify(
{
Root: parent.trace_id,
Parent: subsegment.id,
Sampled: subsegment.notTraced ? '0' : '1',
},
';',
);
if (data != null) {
for (const [key, value] of Object.entries(data)) {
traceHeader += ';' + key +'=' + value;
}
}
if (!segment.noOp) {
args.request.headers['X-Amzn-Trace-Id'] = traceHeader;
}
let res;
try {
res = await next(args);
if (!res) {
throw new Error('Failed to get response from instrumented AWS Client.');
}
const [aws, http] = await buildAttributesFromMetadata(
service,
operation,
await config.region(),
commandInput,
res,
null,
);
subsegment.addAttribute('aws', aws);
subsegment.addAttribute('http', http);
addFlags(http, subsegment);
subsegment.close();
return res;
} catch (err: any) {
if (err.$metadata) {
const [aws, http] = await buildAttributesFromMetadata(
service,
operation,
await config.region(),
commandInput,
null,
err,
);
subsegment.addAttribute('aws', aws);
subsegment.addAttribute('http', http);
addFlags(http, subsegment, err);
}
const errObj = { message: err.message, name: err.name, stack: err.stack || new Error().stack };
subsegment.close(errObj, true);
throw err;
}
};
const xRayMiddlewareOptions: BuildHandlerOptions = {
name: XRAY_PLUGIN_NAME,
step: 'build',
};
const getXRayPlugin = (config: RegionResolvedConfig, manualSegment?: SegmentLike): Pluggable<any, any> => ({
applyToStack: (stack: MiddlewareStack<any, any>) => {
stack.add(getXRayMiddleware(config, manualSegment), xRayMiddlewareOptions);
},
});
/**
* Instruments AWS SDK V3 clients with X-Ray via middleware.
*
* @param client - AWS SDK V3 client to instrument
* @param manualSegment - Parent segment or subsegment that is passed in for manual mode users
* @returns - the client with the X-Ray instrumentation middleware added to its middleware stack
*/
export function captureAWSClient<T extends { middlewareStack: { remove: any, use: any }, config: any }>(client: T, manualSegment?: SegmentLike): T {
// Remove existing middleware to ensure operation is idempotent
client.middlewareStack.remove(XRAY_PLUGIN_NAME);
client.middlewareStack.use(getXRayPlugin(client.config, manualSegment));
return client;
}