Skip to content

Tracer

Tracer is an opinionated thin wrapper for AWS X-Ray SDK for Node.js.

Key features

  • Auto-capturing cold start and service name as annotations, and responses or full exceptions as metadata.
  • Automatically tracing HTTP(S) clients including fetch and generating segments for each request.
  • Supporting tracing functions via decorators, middleware, and manual instrumentation.
  • Supporting tracing AWS SDK v2 and v3 via AWS X-Ray SDK for Node.js.
  • Auto-disable tracing when not running in the Lambda environment.


Screenshot of the Amazon CloudWatch Console showing an example of segments and subsegments generated and with annotations set for the handler
Tracer showcase - Handler Annotations

Getting started

Tracer relies on AWS X-Ray SDK over OpenTelemetry Distro (ADOT) for optimal cold start (lower latency).

Installation

Install the library in your project:

1
npm install @aws-lambda-powertools/tracer

Usage

The Tracer utility must always be instantiated outside of the Lambda handler. In doing this, subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time. In addition, Tracer can track cold start and annotate the traces accordingly.

1
2
3
4
5
6
7
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

export const handler = async (_event, _context): Promise<void> => {
  tracer.getSegment();
};

Using with ESM?

Tracer relies on the AWS X-Ray SDK for Node.js, which is distributed as a CommonJS module and uses require.

To use it in an ESM project, you can instruct your bundler to use the require syntax for specific dependencies while using ESM for everything else. This is commonly known as polyfill.

Code snippets for AWS CDK and AWS SAM CLI with esbuild
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Stack, type StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';

export class MyStack extends Stack {
public constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const handler = new NodejsFunction(this, 'helloWorldFunction', {
    runtime: Runtime.NODEJS_20_X,
    handler: 'handler',
    entry: 'src/index.ts',
    bundling: {
        format: OutputFormat.ESM,
        minify: true,
        esbuildArgs: {
        "--tree-shaking": "true",
        },
        banner: 
        "import { createRequire } from 'module';const require = createRequire(import.meta.url);", // (1)!
    },
    });
}
}
  1. esbuild will include this arbitrary code at the top of your bundle to maximize CommonJS compatibility (require keyword).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Transform: AWS::Serverless-2016-10-31
Resources:
HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
    Runtime: nodejs20.x
    Handler: src/index.handler
    Metadata:
    BuildMethod: esbuild
    BuildProperties:
        Minify: true
        Target: 'ES2020'
        Sourcemap: true
        Format: esm
        EntryPoints:
        - src/index.ts
        Banner:
        js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);"  # (1)!
  1. esbuild will include this arbitrary code at the top of your bundle to maximize CommonJS compatibility (require keyword).

Utility settings

The library has three optional settings. You can set them as environment variables, or pass them in the constructor:

Setting Description Environment variable Default Allowed Values Example Constructor parameter
Service name Sets an annotation with the name of the service across all traces POWERTOOLS_SERVICE_NAME service_undefined Any string serverlessAirline serviceName
Tracing enabled Enables or disables tracing. POWERTOOLS_TRACE_ENABLED true true or false false enabled
Capture HTTPs Requests Defines whether HTTPs requests will be traced or not POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS true true or false false captureHTTPsRequests
Capture Response Defines whether functions responses are serialized as metadata POWERTOOLS_TRACER_CAPTURE_RESPONSE true true or false false captureResult
Capture Errors Defines whether functions errors are serialized as metadata POWERTOOLS_TRACER_CAPTURE_ERROR true true or false false N/A

Note

Before you use this utility, your AWS Lambda function must have Active Tracing enabled as well as have permissions to send traces to AWS X-Ray

Example using AWS Serverless Application Model (SAM)

The Tracer utility is instantiated outside of the Lambda handler. In doing this, the same instance can be used across multiple invocations inside the same execution environment. This allows Tracer to be aware of things like whether or not a given invocation had a cold start or not.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Tracer } from '@aws-lambda-powertools/tracer';

// Tracer parameter fetched from the environment variables (see template.yaml tab)
const tracer = new Tracer();
tracer.getSegment();

// You can also pass the parameter in the constructor
// const tracer = new Tracer({
//     serviceName: 'serverlessAirline'
// });
1
2
3
4
5
6
7
8
9
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs20.x
      Tracing: Active
      Environment:
        Variables:
          POWERTOOLS_SERVICE_NAME: serverlessAirline

Lambda handler

You can quickly start by importing the Tracer class, initialize it outside the Lambda handler, and instrument your function.

A note about Middy

We guarantee support for both Middy.js v4.x & v5.x with the latter being available only if you are using ES modules. Check their docs to learn more about Middy and its middleware stack as well as best practices when working with Powertools.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Tracer } from '@aws-lambda-powertools/tracer';
import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware';
import middy from '@middy/core';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  tracer.putAnnotation('successfulBooking', true);
};

// Wrap the handler with middy
export const handler = middy(lambdaHandler)
  // Use the middleware by passing the Tracer instance as a parameter
  .use(captureLambdaHandler(tracer));

Note

The class method decorators in this project follow the experimental implementation enabled via the experimentalDecorators compiler option in TypeScript.

Additionally, they are implemented to decorate async methods. When decorating a synchronous one, the decorator replaces its implementation with an async one causing the caller to have to await the now decorated method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

class Lambda implements LambdaInterface {
  // Decorate your handler class method
  @tracer.captureLambdaHandler()
  public async handler(_event: unknown, _context: unknown): Promise<void> {
    tracer.getSegment();
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
  1. Binding your handler method allows your handler to access this.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { Tracer } from '@aws-lambda-powertools/tracer';
import type { Subsegment } from 'aws-xray-sdk-core';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<unknown> => {
  const segment = tracer.getSegment(); // This is the facade segment (the one that is created by AWS Lambda)
  let subsegment: Subsegment | undefined;
  if (segment) {
    // Create subsegment for the function & set it as active
    subsegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`);
    tracer.setSegment(subsegment);
  }

  // Annotate the subsegment with the cold start & serviceName
  tracer.annotateColdStart();
  tracer.addServiceNameAnnotation();

  try {
    // Add the response as metadata
    tracer.addResponseAsMetadata({}, process.env._HANDLER);
  } catch (err) {
    // Add the error as metadata
    tracer.addErrorAsMetadata(err as Error);
    throw err;
  } finally {
    if (segment && subsegment) {
      // Close subsegment (the AWS Lambda one is closed automatically)
      subsegment.close();
      // Set back the facade segment as active again
      tracer.setSegment(segment);
    }
  }

  return {};
};

When using the captureLambdaHandler decorator or middleware, Tracer performs these additional tasks to ease operations:

  • Handles the lifecycle of the subsegment
  • Creates a ColdStart annotation to easily filter traces that have had an initialization overhead
  • Creates a Service annotation to easily filter traces that have a specific service name
  • Captures any response, or full exceptions generated by the handler, and include them as tracing metadata

Annotations & Metadata

Annotations are key-values associated with traces and indexed by AWS X-Ray. You can use them to filter traces and to create Trace Groups to slice and dice your transactions.

Metadata are key-values also associated with traces but not indexed by AWS X-Ray. You can use them to add additional context for an operation using any native object.

You can add annotations using putAnnotation method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  const handlerSegment = tracer.getSegment()?.addNewSubsegment('### handler');
  handlerSegment && tracer.setSegment(handlerSegment); // (1)!

  tracer.putAnnotation('successfulBooking', true);

  handlerSegment?.close();
  handlerSegment && tracer.setSegment(handlerSegment?.parent); // (2)!
};
  1. When Lambda starts an invocation the X-Ray SDk creates a segment called facade. This segment cannot be annotated or modified by your code, so you need to create a new subsegment. This is done automatically by Tracer when using the decorator or middleware patterns
  2. To correctly trace the current and subsequent invocations you need to restore the original segment, this is done automatically by Tracer when using the decorator or middleware patterns.

You can add metadata using putMetadata method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  const handlerSegment = tracer.getSegment()?.addNewSubsegment('### handler');
  handlerSegment && tracer.setSegment(handlerSegment); // (1)!

  tracer.putMetadata('paymentResponse', {
    foo: 'bar',
  });

  handlerSegment?.close();
  handlerSegment && tracer.setSegment(handlerSegment?.parent); // (2)!
};
  1. When Lambda starts an invocation the X-Ray SDk creates a segment called facade. This segment cannot be modified by your code, so you need to create a new subsegment. This is done automatically by Tracer when using the decorator or middleware patterns
  2. To correctly trace the current and subsequent invocations you need to restore the original segment, this is done automatically by Tracer when using the decorator or middleware patterns.
Screenshot of the Amazon CloudWatch Console showing an example of segments and subsegments generated and with metadata set for the handler
Tracer showcase - Handler Metadata

Methods

You can trace other class methods using the captureMethod decorator or any arbitrary asynchronous function using manual instrumentation.

Note

The class method decorators in this project follow the experimental implementation enabled via the experimentalDecorators compiler option in TypeScript.

Additionally, they are implemented to decorate async methods. When decorating a synchronous one, the decorator replaces its implementation with an async one causing the caller to have to await the now decorated method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

class Lambda implements LambdaInterface {
  // Decorate your class method
  @tracer.captureMethod() // (1)
  public async getChargeId(): Promise<string> {
    /* ... */
    return 'foo bar';
  }

  public async handler(_event: unknown, _context: unknown): Promise<void> {
    await this.getChargeId();
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (2)
  1. You can set a custom name for the subsegment by passing subSegmentName to the decorator, like: @tracer.captureMethod({ subSegmentName: '### myCustomMethod' }).
  2. Binding your handler method allows your handler to access this.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Tracer } from '@aws-lambda-powertools/tracer';
import type { Subsegment } from 'aws-xray-sdk-core';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

const getChargeId = async (): Promise<unknown> => {
  const parentSubsegment = tracer.getSegment(); // This is the subsegment currently active
  let subsegment: Subsegment | undefined;
  if (parentSubsegment) {
    // Create subsegment for the function & set it as active
    subsegment = parentSubsegment.addNewSubsegment('### chargeId');
    tracer.setSegment(subsegment);
  }

  let res: unknown;
  try {
    /* ... */
    // Add the response as metadata
    tracer.addResponseAsMetadata(res, 'chargeId');
  } catch (err) {
    // Add the error as metadata
    tracer.addErrorAsMetadata(err as Error);
    throw err;
  }

  if (parentSubsegment && subsegment) {
    // Close subsegment (the AWS Lambda one is closed automatically)
    subsegment.close();
    // Set the facade segment as active again
    tracer.setSegment(parentSubsegment);
  }

  return res;
};

export const handler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  await getChargeId();
};

Patching AWS SDK clients

Tracer can patch any AWS SDK clients and create traces when your application makes calls to AWS services.

Info

The following snippet assumes you are using the AWS SDK v3 for JavaScript

You can patch any AWS SDK clients by calling the captureAWSv3Client method:

1
2
3
4
5
6
7
8
import { Tracer } from '@aws-lambda-powertools/tracer';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });
// Instrument the AWS SDK client
const client = tracer.captureAWSv3Client(new SecretsManagerClient({}));

export default client;

Info

The following two snippets assume you are using the AWS SDK v2 for JavaScript

You can patch all AWS SDK v2 clients by calling the captureAWS method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Tracer } from '@aws-lambda-powertools/tracer';
import AWS from 'aws-sdk';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

// Instrument all AWS SDK clients created from this point onwards
tracer.captureAWS(AWS);

// Create a new client which will be automatically instrumented
const client = new AWS.SecretsManager();
export default client;

If you're looking to shave a few microseconds, or milliseconds depending on your function memory configuration, you can patch only specific AWS SDK v2 clients using captureAWSClient:

1
2
3
4
5
6
7
8
import { Tracer } from '@aws-lambda-powertools/tracer';
import { S3 } from 'aws-sdk';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });
// Instrument the AWS SDK client
const client = tracer.captureAWSClient(new S3());

export default client;

Tracing HTTP requests

When your function makes outgoing requests to APIs, Tracer automatically traces those calls and adds the API to the service graph as a downstream service.

You can opt-out from this feature by setting the POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS=false environment variable or by passing the captureHTTPSRequests: false option to the Tracer constructor.

Info

The following snippet shows how to trace fetch requests, but you can use any HTTP client library built on top it, or on http, and https. Support to 3rd party HTTP clients is provided on a best effort basis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Tracer } from '@aws-lambda-powertools/tracer';
import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware';
import middy from '@middy/core';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

export const handler = middy(
  async (_event: unknown, _context: unknown): Promise<void> => {
    await fetch('https://httpbin.org/status/200');
  }
).use(captureLambdaHandler(tracer));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
    "id": "22883fbc730e3a0b",
    "name": "## index.handler",
    "start_time": 1647956168.22749,
    "end_time": 1647956169.0679862,
    "subsegments": [
        {
            "id": "ab82ab2b7d525d8f",
            "name": "httpbin.org",
            "start_time": 1647956168.407,
            "end_time": 1647956168.945,
            "http": {
                "request": {
                    "url": "https://httpbin.org/status/200",
                    "method": "GET"
                },
                "response": {
                    "status": 200,
                    "content_length": 0
                }
            },
            "namespace": "remote"
        }
    ]
}

Advanced

Disabling response auto-capture

Use POWERTOOLS_TRACER_CAPTURE_RESPONSE=false environment variable to instruct Tracer not to serialize function responses as metadata.

This is commonly useful in three scenarios

  1. You might return sensitive information you don't want it to be added to your traces
  2. You might manipulate streaming objects that can be read only once; this prevents subsequent calls from being empty
  3. You might return more than 64K of data e.g., message too long error

Alternatively, use the captureResponse: false option in both tracer.captureLambdaHandler() and tracer.captureMethod() decorators, or use the same option in the Middy captureLambdaHandler middleware to instruct Tracer not to serialize function responses as metadata.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

class Lambda implements LambdaInterface {
  @tracer.captureMethod({ captureResponse: false })
  public async getChargeId(): Promise<string> {
    /* ... */
    return 'foo bar';
  }

  public async handler(_event: unknown, _context: unknown): Promise<void> {
    /* ... */
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

class Lambda implements LambdaInterface {
  @tracer.captureLambdaHandler({ captureResponse: false })
  public async handler(_event: unknown, _context: unknown): Promise<void> {
    tracer.getSegment();
  }
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Tracer } from '@aws-lambda-powertools/tracer';
import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware';
import middy from '@middy/core';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

const lambdaHandler = async (
  _event: unknown,
  _context: unknown
): Promise<void> => {
  /* ... */
};

// Wrap the handler with middy
export const handler = middy(lambdaHandler)
  // Use the middleware by passing the Tracer instance as a parameter,
  // but specify the captureResponse option as false.
  .use(captureLambdaHandler(tracer, { captureResponse: false }));

Disabling errors auto-capture

Use POWERTOOLS_TRACER_CAPTURE_ERROR=false environment variable to instruct Tracer not to serialize errors as metadata.

Commonly useful in one scenario

  1. You might return sensitive information from errors, stack traces you might not control

Access AWS X-Ray Root Trace ID

Tracer exposes a getRootXrayTraceId() method that allows you to retrieve the AWS X-Ray Root Trace ID corresponds to the current function execution.

This is commonly useful in two scenarios

  1. By including the root trace id in your response, consumers can use it to correlate requests
  2. You might want to surface the root trace id to your end users so that they can reference it while contacting customer service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Tracer } from '@aws-lambda-powertools/tracer';

const tracer = new Tracer({ serviceName: 'serverlessAirline' });

export const handler = async (): Promise<unknown> => {
  try {
    throw new Error('Something went wrong');
  } catch (err) {
    const rootTraceId = tracer.getRootXrayTraceId();

    // Example of returning an error response
    return {
      statusCode: 500,
      body: `Internal Error - Please contact support and quote the following id: ${rootTraceId}`,
      headers: { _X_AMZN_TRACE_ID: rootTraceId },
    };
  }
};

Escape hatch mechanism

You can use tracer.provider attribute to access a subset of the methods provided by the AWS X-Ray SDK.

This is useful when you need a feature available in X-Ray that is not available in the Tracer utility, for example SQL queries tracing, or a custom logger.

1
2
3
4
5
6
7
import { Logger } from '@aws-lambda-powertools/logger';
import { Tracer } from '@aws-lambda-powertools/tracer';

const serviceName = 'serverlessAirline';
const logger = new Logger({ serviceName: serviceName });
const tracer = new Tracer({ serviceName: serviceName });
tracer.provider.setLogger(logger);

If you need to access a method that is not available you can import it directly from the AWS X-Ray SDK for Node.js. Compatibility with the Tracer utility is not guaranteed.

Testing your code

Tracer is disabled by default when not running in the AWS Lambda environment - This means no code changes or environment variables to be set.

Tips

  • Use annotations on key operations to slice and dice traces, create unique views, and create metrics from it via Trace Groups
  • Use a namespace when adding metadata to group data more easily
  • Annotations and metadata are added to the currently open subsegment. If you want them in a specific subsegment, create one via the escape hatch mechanism