@aws-lambda-powertools/tracer
Version:
The tracer package for the Powertools for AWS Lambda (TypeScript) library
238 lines (237 loc) • 10.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProviderService = void 0;
const aws_xray_sdk_core_1 = __importDefault(require("aws-xray-sdk-core"));
const { captureAWS, captureAWSClient, captureAWSv3Client, captureAsyncFunc, captureFunc, captureHTTPsGlobal, getNamespace, getSegment, setSegment, Segment: XraySegment, setContextMissingStrategy, setDaemonAddress, setLogger, } = aws_xray_sdk_core_1.default;
const node_diagnostics_channel_1 = require("node:diagnostics_channel");
const node_http_1 = __importDefault(require("node:http"));
const node_https_1 = __importDefault(require("node:https"));
require("@aws/lambda-invoke-store");
const commons_1 = require("@aws-lambda-powertools/commons");
const env_1 = require("@aws-lambda-powertools/commons/utils/env");
const utilities_js_1 = require("./utilities.js");
/**
* The ProviderService class is a wrapper around the AWS X-Ray SDK for Node.js.
*
* The service provides exposes a selection of methods to interact with the SDK directly,
* these methods were chosen to be the most useful in the context of a Lambda function and
* have been tested to work with Powertools for AWS Lambda.
*
* If you want to use one of the other methods that are not exposed by this class,
* you can import the methods directly from the `aws-xray-sdk-core` package, and for most cases,
* they should work as expected. However, support for these methods is not guaranteed.
*/
class ProviderService {
/**
* Key used to store the active segment in the Lambda Invoke Store when
* multiple invocations may be multiplexed into the same execution
* environment (Lambda Managed Instances with
* `perExecutionEnvironmentMaxConcurrency` > 1).
*
* In Lambda mode the X-Ray SDK keeps the active segment in a single
* process-wide CLS context shared by all concurrent invocations, so
* writing to it from one invocation clobbers the others. The Invoke Store
* is scoped to the invocation by the runtime, so it provides the isolation
* the SDK context can't.
*/
#segmentKey = Symbol('powertools.tracer.segment');
#getInvokeStore() {
const store = globalThis.awslambda?.InvokeStore;
if (store === undefined) {
throw new Error('InvokeStore is not available');
}
return store;
}
/**
* @deprecated Use {@link captureAWSv3Client} instead.
*/
/* v8 ignore next -- @preserve */ captureAWS(awssdk) {
return captureAWS(awssdk);
}
/**
* @deprecated Use {@link captureAWSv3Client} instead.
*/
/* v8 ignore next -- @preserve */ captureAWSClient(service) {
return captureAWSClient(service);
}
captureAWSv3Client(service) {
(0, commons_1.addUserAgentMiddleware)(service, 'tracer');
// biome-ignore lint/suspicious/noExplicitAny: Type must be aliased as any because of this https://github.com/aws/aws-xray-sdk-node/issues/439#issuecomment-859715660
return captureAWSv3Client(service);
}
captureAsyncFunc(name, fcn, _parent) {
return captureAsyncFunc(name, fcn);
}
captureFunc(name, fcn, _parent) {
return captureFunc(name, fcn);
}
captureHTTPsGlobal() {
captureHTTPsGlobal(node_http_1.default);
captureHTTPsGlobal(node_https_1.default);
}
getNamespace() {
return getNamespace();
}
getSegment() {
if ((0, env_1.shouldUseInvokeStore)()) {
// The facade segment isn't stored per-invocation: it's created by the
// X-Ray SDK at init and lives in its process-wide context, so when the
// current invocation hasn't set an active segment yet we fall through
// to the SDK
const segment = this.#getInvokeStore().get(this.#segmentKey);
if (segment !== undefined) {
return segment;
}
}
return getSegment();
}
/**
* Instrument `fetch` requests with AWS X-Ray
*
* The instrumentation is done by subscribing to the `undici` events. When a request is created,
* a new subsegment is created with the hostname of the request.
*
* Then, when the headers are received, the subsegment is updated with the request and response details.
*
* Finally, when the request is completed, the subsegment is closed.
*
* @see {@link https://nodejs.org/api/diagnostics_channel.html#diagnostics_channel_channel_publish | Diagnostics Channel - Node.js Documentation}
*/
instrumentFetch() {
/**
* Create a segment at the start of a request made with `undici` or `fetch`.
*
* That `message` must be `unknown` because that's the type expected by `subscribe`
*
* @param message The message received from the `undici` channel
*/
const onRequestStart = (message) => {
const { request } = message;
const method = request.method;
if (method === 'CONNECT')
return;
const parentSubsegment = this.getSegment();
const requestURL = (0, utilities_js_1.getRequestURL)(request);
if (parentSubsegment && requestURL) {
const subsegment = parentSubsegment.addNewSubsegment(requestURL.hostname);
subsegment.addAttribute('namespace', 'remote');
// addHeader is not part of the type definition but it's available https://github.com/nodejs/undici/blob/main/docs/docs/api/DiagnosticsChannel.md#undicirequestcreate
// @ts-expect-error
request.addHeader('X-Amzn-Trace-Id', `Root=${(0, env_1.getXRayTraceIdFromEnv)()};Parent=${subsegment.id};Sampled=${subsegment.notTraced ? '0' : '1'}`);
subsegment.http = {
request: {
url: `${requestURL.protocol}//${requestURL.hostname}${requestURL.pathname}`,
method,
},
};
this.setSegment(subsegment);
}
};
/**
* Enrich the subsegment with the response details, and close it.
* Then, set the parent segment as the active segment.
*
* `message` must be `unknown` because that's the type expected by `subscribe`
*
* @param message The message received from the `undici` channel
*/
const onResponse = (message) => {
const { response } = message;
const subsegment = this.getSegment();
if ((0, utilities_js_1.isHttpSubsegment)(subsegment)) {
const status = response.statusCode;
const contentLenght = (0, utilities_js_1.findHeaderAndDecode)(response.headers, 'content-length');
subsegment.http = {
...subsegment.http,
response: {
status,
...(contentLenght && {
content_length: Number.parseInt(contentLenght, 10),
}),
},
};
if (status === 429) {
subsegment.addThrottleFlag();
}
if (status >= 400 && status < 500) {
subsegment.addErrorFlag();
}
else if (status >= 500 && status < 600) {
subsegment.addFaultFlag();
}
subsegment.close();
this.setSegment(subsegment.parent);
}
};
/**
* Add an error to the subsegment when the request fails.
*
* This is used to handle the case when the request fails to establish a connection with the server or timeouts.
* In all other cases, for example, when the server returns a 4xx or 5xx status code, the error is added in the `onResponse` function.
*
* that `message` must be `unknown` because that's the type expected by `subscribe`
*
* @param message The message received from the `undici` channel
*/
const onError = (message) => {
const { error } = message;
const subsegment = this.getSegment();
if ((0, utilities_js_1.isHttpSubsegment)(subsegment)) {
subsegment.addErrorFlag();
error instanceof Error && subsegment.addError(error, true);
subsegment.close();
this.setSegment(subsegment.parent);
}
};
(0, node_diagnostics_channel_1.subscribe)('undici:request:create', onRequestStart);
(0, node_diagnostics_channel_1.subscribe)('undici:request:headers', onResponse);
(0, node_diagnostics_channel_1.subscribe)('undici:request:error', onError);
}
putAnnotation(key, value) {
const segment = this.getSegment();
if (segment === undefined) {
console.warn('No active segment or subsegment found, skipping annotation');
return;
}
if (segment instanceof XraySegment) {
console.warn('You cannot annotate the main segment in a Lambda execution environment');
return;
}
segment.addAnnotation(key, value);
}
putMetadata(key, value, namespace) {
const segment = this.getSegment();
if (segment === undefined) {
console.warn('No active segment or subsegment found, skipping metadata addition');
return;
}
if (segment instanceof XraySegment) {
console.warn('You cannot add metadata to the main segment in a Lambda execution environment');
return;
}
segment.addMetadata(key, value, namespace);
}
setContextMissingStrategy(strategy) {
setContextMissingStrategy(strategy);
}
setDaemonAddress(address) {
setDaemonAddress(address);
}
setLogger(logObj) {
setLogger(logObj);
}
setSegment(segment) {
if ((0, env_1.shouldUseInvokeStore)()) {
// Writing to the SDK context here would clobber the shared slot for
// every other in-flight invocation, so the active segment goes in the
// invocation-scoped store instead
this.#getInvokeStore().set(this.#segmentKey, segment);
return;
}
setSegment(segment);
}
}
exports.ProviderService = ProviderService;