@aws-lambda-powertools/tracer
Version:
The tracer package for the Powertools for AWS Lambda (TypeScript) library
192 lines (191 loc) • 8.12 kB
JavaScript
import xraySdk from 'aws-xray-sdk-core';
const { captureAWS, captureAWSClient, captureAWSv3Client, captureAsyncFunc, captureFunc, captureHTTPsGlobal, getNamespace, getSegment, setSegment, Segment: XraySegment, setContextMissingStrategy, setDaemonAddress, setLogger, } = xraySdk;
import { subscribe } from 'node:diagnostics_channel';
import http from 'node:http';
import https from 'node:https';
import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons';
import { environmentVariablesService } from '../config/EnvironmentVariablesService.js';
import { findHeaderAndDecode, getRequestURL, isHttpSubsegment, } from './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 {
/**
* @deprecated
*/
captureAWS(awssdk) {
return captureAWS(awssdk);
}
/**
* @deprecated
*/
captureAWSClient(service) {
return captureAWSClient(service);
}
captureAWSv3Client(service) {
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(http);
captureHTTPsGlobal(https);
}
getNamespace() {
return getNamespace();
}
getSegment() {
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 parentSubsegment = this.getSegment();
const requestURL = getRequestURL(request);
if (parentSubsegment && requestURL) {
const method = request.method;
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=${environmentVariablesService.getXrayTraceId()};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 (isHttpSubsegment(subsegment)) {
const status = response.statusCode;
const contentLenght = findHeaderAndDecode(response.headers, 'content-length');
subsegment.http = {
...subsegment.http,
response: {
status,
...(contentLenght && {
content_length: Number.parseInt(contentLenght),
}),
},
};
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 (isHttpSubsegment(subsegment)) {
subsegment.addErrorFlag();
error instanceof Error && subsegment.addError(error, true);
subsegment.close();
this.setSegment(subsegment.parent);
}
};
subscribe('undici:request:create', onRequestStart);
subscribe('undici:request:headers', onResponse);
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) {
setSegment(segment);
}
}
export { ProviderService };