diff --git a/docs/core/tracer.md b/docs/core/tracer.md index 424f54caf4..21c3bf2cf4 100644 --- a/docs/core/tracer.md +++ b/docs/core/tracer.md @@ -3,7 +3,7 @@ title: Tracer description: Core utility --- -Tracer is an opinionated thin wrapper for [AWS X-Ray Python SDK](https://github.com/aws/aws-xray-sdk-python/). +Tracer is an opinionated thin wrapper for [AWS X-Ray SDK for Node.js](https://github.com/aws/aws-xray-sdk-node). ![Tracer showcase](../media/tracer_utility_showcase.png) @@ -11,8 +11,8 @@ Tracer is an opinionated thin wrapper for [AWS X-Ray Python SDK](https://github. * Auto capture cold start as annotation, and responses or full exceptions as metadata * Auto-disable when not running in AWS Lambda environment -* Support tracing async methods, generators, and context managers -* Auto patch supported modules by AWS X-Ray +* Support tracing functions via decorators, middleware, and manual instrumentation +* Support tracing AWS SDK v2 and v3 via AWS X-Ray SDK for Node.js ## Getting started @@ -38,26 +38,79 @@ Before your use this utility, your AWS Lambda function [must have permissions](h ### Lambda handler -You can quickly start by importing the `Tracer` class, initialize it outside the Lambda handler, and use `capture_lambda_handler` decorator. +You can quickly start by importing the `Tracer` class, initialize it outside the Lambda handler, and instrument your function. -=== "index.ts" +=== "Middleware" + + ```typescript hl_lines="1 3 6" + import { Tracer } from '@aws-lambda-powertools/tracer'; + + const tracer = Tracer(); // Sets service via env var + // OR tracer = Tracer({ service: 'example' }); - ```typescript hl_lines="1 3 6" - import { Tracer } from '@aws-lambda-powertools/tracer'; + // TODO: update example once middleware has been implemented. + + export const handler = async (_event: any, _context: any) => { + ... + } + ``` + +=== "Decorator" + + ```typescript hl_lines="1 3 7" + import { Tracer } from '@aws-lambda-powertools/tracer'; + + const tracer = Tracer(); // Sets service via env var + // OR tracer = Tracer({ service: 'example' }); + + class Lambda { + @tracer.captureLambdaHanlder() + public handler(event: any, context: any) { + ... + } + } + + export const handlerClass = new Lambda(); + export const handler = handlerClass.handler; + ``` - const tracer = Tracer(); // Sets service via env var - // OR tracer = Tracer({ service: 'example' }); +=== "Manual" - @tracer.capture_lambda_handler - const handler = (event, context) => { - const chargeId = event.chargeId; - const payment = collectPayment(chargeId); - ... - } - ``` + ```typescript hl_lines="1-2 4 8-9 11 17 20 24" + import { Tracer } from '@aws-lambda-powertools/tracer'; + import { Segment } from 'aws-xray-sdk-core'; + + const tracer = Tracer(); // Sets service via env var + // OR tracer = Tracer({ service: 'example' }); + + export const handler = async (_event: any, context: any) => { + // Create subsegment & set it as active + const subsegment = new Subsegment(`## ${context.functionName}`); + tracer.setSegment(subsegment); + // Add the ColdStart annotation + this.putAnnotation('ColdStart', tracer.coldStart); + + let res; + try { + res = await someLogic(); // Do something + // Add the response as metadata + tracer.putMetadata(`${context.functionName} response`, data); + } catch (err) { + // Add the error as metadata + subsegment.addError(err, false); + } + + // Close subsegment + subsegment.close(); + + return res; + } + ``` -When using this `capture_lambda_handler` decorator, Tracer performs these additional tasks to ease operations: + +When using thes `captureLambdaHanlder` decorator or the `TBD` 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 * Captures any response, or full exceptions generated by the handler, and include as tracing metadata @@ -70,146 +123,162 @@ When using this `capture_lambda_handler` decorator, Tracer performs these additi === "Annotations" You can add annotations using `putAnnotation` method. - ```typescript hl_lines="7" - import { Tracer } from '@aws-lambda-powertools/tracer'; - - const tracer = Tracer() - - @tracer.capture_lambda_handler - const handler = (event, context) => { - ... - tracer.addAnnotation('payment_response', 'SUCCESS'); - } + ```typescript hl_lines="6" + import { Tracer } from '@aws-lambda-powertools/tracer'; + + const tracer = new Tracer({ serviceName: 'my-service' }); + + export const handler = async (_event: any, _context: any) => { + tracer.putAnnotation('PaymentStatus', "SUCCESS"); + } ``` === "Metadata" - You can add metadata using `putMetadata` method. + You can add metadata using `putMetadata` method. - ```typescript hl_lines="9" + ```typescript hl_lines="7" import { Tracer } from '@aws-lambda-powertools/tracer'; - const tracer = Tracer() + const tracer = new Tracer({ serviceName: 'my-service' }); - @tracer.capture_lambda_handler - const handler = (event, context) => { - ... - const res = someLogic(); - tracer.putMetadata('payment_response', res); - } + export const handler = async (_event: any, _context: any) => { + const res = someLogic(); + tracer.putMetadata('PaymentResponse', res); + } ``` -[//]:# (START EDITING FROM HERE DOWN) - -### Synchronous functions +### Methods -You can trace synchronous functions using the `capture_method` decorator. +You can trace other methods using the `captureMethod` decorator. -!!! warning - **When `capture_response` is enabled, the function response will be read and serialized as json.** +=== "Middleware" - The serialization is performed by the aws-xray-sdk which uses the `jsonpickle` module. This can cause - unintended consequences if there are side effects to recursively reading the returned value, for example if the - decorated function response contains a file-like object or a `StreamingBody` for S3 objects. + ```typescript hl_lines="1 3 6" + import { Tracer } from '@aws-lambda-powertools/tracer'; - ```python hl_lines="7 13" - @tracer.capture_method - def collect_payment(charge_id): - ret = requests.post(PAYMENT_ENDPOINT) # logic - tracer.put_annotation("PAYMENT_STATUS", "SUCCESS") # custom annotation - return ret - ``` + const tracer = Tracer(); -### Asynchronous and generator functions + // TODO: update example once middleware has been implemented. -!!! warning - **We do not support async Lambda handler** - Lambda handler itself must be synchronous -You can trace asynchronous functions and generator functions (including context managers) using `capture_method`. -=== "Async" + export const handler = async (_event: any, _context: any) => { + ... + } + ``` - ```python hl_lines="7" - import asyncio - import contextlib - from aws_lambda_powertools import Tracer +=== "Decorator" - tracer = Tracer() + ```typescript hl_lines="6" + import { Tracer } from '@aws-lambda-powertools/tracer'; - @tracer.capture_method - async def collect_payment(): - ... - ``` + const tracer = Tracer(); -=== "Context manager" + class Lambda { + @tracer.captureMethod() + public getChargeId(): string { + ... + return 'foo bar' + } - ```python hl_lines="7-8" - import asyncio - import contextlib - from aws_lambda_powertools import Tracer + public handler(event: any, context: any) { + const chargeId = this.getChargeId(); + const payment = collectPayment(chargeId); + ... + } + } + + export const handlerClass = new Lambda(); + export const getChargeId = handlerClass.getChargeId; + export const handler = handlerClass.handler; + ``` - tracer = Tracer() +=== "Manual" - @contextlib.contextmanager - @tracer.capture_method - def collect_payment_ctxman(): - yield result + ```typescript hl_lines="2 8-9 15 18 22" + import { Tracer } from '@aws-lambda-powertools/tracer'; + import { Segment } from 'aws-xray-sdk-core'; + + const tracer = new Tracer({ serviceName: 'my-service' }); + + const chargeId = async () => { + // Create subsegment & set it as active + const subsegment = new Subsegment(`### chargeId`); + tracer.setSegment(subsegment); + + let res; + try { + res = await someLogic(); // Do something + // Add the response as metadata + tracer.putMetadata(`chargeId response`, data); + } catch (err) { + // Add the error as metadata + subsegment.addError(err, false); + } + + // Close subsegment + subsegment.close(); + + return res; + } + + export const handler = async (_event: any, _context: any) => { + const chargeId = this.getChargeId(); + const payment = collectPayment(chargeId); ... + } ``` -=== "Generators" - - ```python hl_lines="9" - import asyncio - import contextlib - from aws_lambda_powertools import Tracer +## Advanced - tracer = Tracer() +### Patching AWS SDK clients - @tracer.capture_method - def collect_payment_gen(): - yield result - ... - ``` +Tracer can patch [AWS SDK clients](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html) and create traces when your application makes calls to AWS services. -The decorator will detect whether your function is asynchronous, a generator, or a context manager and adapt its behaviour accordingly. +!!! info + The following snippet assumes you are using [**AWS SDK v3** for JavaScript](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/) -=== "app.py" +You can patch any AWS SDK clients by calling `captureAWSv3Client` method: - ```python - @tracer.capture_lambda_handler - def handler(evt, ctx): - asyncio.run(collect_payment()) +=== "index.ts" - with collect_payment_ctxman as result: - do_something_with(result) + ```typescript hl_lines="6" + import { S3Client } from "@aws-sdk/client-s3"; + import { Tracer } from '@aws-lambda-powertools/tracer'; - another_result = list(collect_payment_gen()) + const tracer = new Tracer(); + const client = new S3Client({}); + tracer.captureAWSv3Client(client); ``` -## Advanced +!!! info + The following two snippets assume you are using [**AWS SDK v2** for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/welcome.html) -### Patching modules +You can patch all AWS SDK clients by calling `captureAWS` method: -Tracer automatically patches all [supported libraries by X-Ray](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-patching.html) during initialization, by default. Underneath, AWS X-Ray SDK checks whether a supported library has been imported before patching. +=== "index.ts" + + ```typescript hl_lines="4" + import { Tracer } from '@aws-lambda-powertools/tracer'; -If you're looking to shave a few microseconds, or milliseconds depending on your function memory configuration, you can patch specific modules using `patch_modules` param: + const tracer = new Tracer(); + const AWS = tracer.captureAWS(require('aws-sdk')); + ``` -=== "app.py" +If you're looking to shave a few microseconds, or milliseconds depending on your function memory configuration, you can patch specific clients using `captureAWSClient`: - ```python hl_lines="7" - import boto3 - import requests +=== "index.ts" - from aws_lambda_powertools import Tracer + ```typescript hl_lines="5" + import { S3 } from "aws-sdk"; + import { Tracer } from '@aws-lambda-powertools/tracer'; - modules_to_be_patched = ["boto3", "requests"] - tracer = Tracer(patch_modules=modules_to_be_patched) + const tracer = new Tracer(); + const s3 = tracer.captureAWSClient(new S3({ apiVersion: "2006-03-01" })); ``` ### Disabling response auto-capture -> New in 1.9.0 - -Use **`capture_response=False`** parameter in both `capture_lambda_handler` and `capture_method` decorators to instruct Tracer **not** to serialize function responses as metadata. +Use **`POWERTOOLS_TRACER_CAPTURE_RESPONSE=false`** environment variable to instruct Tracer **not** to serialize function responses as metadata. !!! info "This is commonly useful in three scenarios" @@ -217,159 +286,29 @@ Use **`capture_response=False`** parameter in both `capture_lambda_handler` and 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_ -=== "sensitive_data_scenario.py" - - ```python hl_lines="3 7" - from aws_lambda_powertools import Tracer - - @tracer.capture_method(capture_response=False) - def fetch_sensitive_information(): - return "sensitive_information" - - @tracer.capture_lambda_handler(capture_response=False) - def handler(event, context): - sensitive_information = fetch_sensitive_information() - ``` -=== "streaming_object_scenario.py" - - ```python hl_lines="3" - from aws_lambda_powertools import Tracer - - @tracer.capture_method(capture_response=False) - def get_s3_object(bucket_name, object_key): - s3 = boto3.client("s3") - s3_object = get_object(Bucket=bucket_name, Key=object_key) - return s3_object - ``` - ### Disabling exception auto-capture -> New in 1.10.0 - -Use **`capture_error=False`** parameter in both `capture_lambda_handler` and `capture_method` decorators to instruct Tracer **not** to serialize exceptions as metadata. +Use **`POWERTOOLS_TRACER_CAPTURE_ERROR=false`** environment variable to instruct Tracer **not** to serialize exceptions as metadata. !!! info "Commonly useful in one scenario" 1. You might **return sensitive** information from exceptions, stack traces you might not control -=== "sensitive_data_exception.py" - - ```python hl_lines="3 5" - from aws_lambda_powertools import Tracer - - @tracer.capture_lambda_handler(capture_error=False) - def handler(event, context): - raise ValueError("some sensitive info in the stack trace...") - ``` - -### Tracing aiohttp requests - -!!! info - This snippet assumes you have **aiohttp** as a dependency - -You can use `aiohttp_trace_config` function to create a valid [aiohttp trace_config object](https://docs.aiohttp.org/en/stable/tracing_reference.html). This is necessary since X-Ray utilizes aiohttp trace hooks to capture requests end-to-end. - -=== "aiohttp_example.py" - - ```python hl_lines="5 10" - import asyncio - import aiohttp - - from aws_lambda_powertools import Tracer - from aws_lambda_powertools.tracing import aiohttp_trace_config - - tracer = Tracer() - - async def aiohttp_task(): - async with aiohttp.ClientSession(trace_configs=[aiohttp_trace_config()]) as session: - async with session.get("https://httpbin.org/json") as resp: - resp = await resp.json() - return resp - ``` - ### Escape hatch mechanism -You can use `tracer.provider` attribute to access all methods provided by AWS X-Ray `xray_recorder` object. - -This is useful when you need a feature available in X-Ray that is not available in the Tracer utility, for example [thread-safe](https://github.com/aws/aws-xray-sdk-python/#user-content-trace-threadpoolexecutor), or [context managers](https://github.com/aws/aws-xray-sdk-python/#user-content-start-a-custom-segmentsubsegment). - -=== "escape_hatch_context_manager_example.py" +You can use `tracer.provider` attribute to access all methods provided by the [AWS X-Ray SDK](https://docs.aws.amazon.com/xray-sdk-for-nodejs/latest/reference/AWSXRay.html). - ```python hl_lines="7" - from aws_lambda_powertools import Tracer - - tracer = Tracer() - - @tracer.capture_lambda_handler - def handler(event, context): - with tracer.provider.in_subsegment('## custom subsegment') as subsegment: - ret = some_work() - subsegment.put_metadata('response', ret) - ``` +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](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-sqlclients.html), or [a custom logger](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-configuration.html#xray-sdk-nodejs-configuration-logging). -### Concurrent asynchronous functions - -!!! warning - [As of now, X-Ray SDK will raise an exception when async functions are run and traced concurrently](https://github.com/aws/aws-xray-sdk-python/issues/164) - -A safe workaround mechanism is to use `in_subsegment_async` available via Tracer escape hatch (`tracer.provider`). - -=== "concurrent_async_workaround.py" - - ```python hl_lines="6 7 12 15 17" - import asyncio - - from aws_lambda_powertools import Tracer - tracer = Tracer() - - async def another_async_task(): - async with tracer.provider.in_subsegment_async("## another_async_task") as subsegment: - subsegment.put_annotation(key="key", value="value") - subsegment.put_metadata(key="key", value="value", namespace="namespace") - ... - - async def another_async_task_2(): - ... - - @tracer.capture_method - async def collect_payment(charge_id): - asyncio.gather(another_async_task(), another_async_task_2()) - ... - ``` - -### Reusing Tracer across your code - -Tracer keeps a copy of its configuration after the first initialization. This is useful for scenarios where you want to use Tracer in more than one location across your code base. - -!!! warning - When reusing Tracer in Lambda Layers, or in multiple modules, **do not set `auto_patch=False`**, because import order matters. - - This can result in the first Tracer config being inherited by new instances, and their modules not being patched. - -=== "handler.py" - - ```python hl_lines="2 4 9" - from aws_lambda_powertools import Tracer - from payment import collect_payment - - tracer = Tracer(service="payment") - - @tracer.capture_lambda_handler - def handler(event, context): - charge_id = event.get('charge_id') - payment = collect_payment(charge_id) - ``` -=== "payment.py" - A new instance of Tracer will be created but will reuse the previous Tracer instance configuration, similar to a Singleton. - - ```python hl_lines="3 5" - from aws_lambda_powertools import Tracer +=== "index.ts" - tracer = Tracer(service="payment") + ```typescript hl_lines="6" + import { Logger } from '@aws-lambda-powertools/logger'; + import { Tracer } from '@aws-lambda-powertools/tracer'; - @tracer.capture_method - def collect_payment(charge_id: str): - ... + const logger = new Logger(); + const tracer = new Tracer() + tracer.provider.setLogger(logger) ``` ## Testing your code @@ -378,6 +317,6 @@ Tracer is disabled by default when not running in the AWS Lambda environment - T ## 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 current subsegment opened. If you want them in a specific subsegment, use a [context manager](https://github.com/aws/aws-xray-sdk-python/#start-a-custom-segmentsubsegment) via the escape hatch mechanism +- 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 current subsegment opened. If you want them in a specific subsegment, [create one](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html#xray-sdk-nodejs-subsegments-lambda) via the escape hatch mechanism diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 70e22f248c..582d1777dd 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -26,8 +26,8 @@ }, "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/master/packages/tracer#readme", "license": "MIT", - "main": "./lib/index.js", - "types": "./lib/index.d.ts", + "main": "./lib/packages/tracing/src/index.js", + "types": "./lib/packages/tracing/src/index.d.ts", "devDependencies": { "@types/aws-lambda": "^8.10.72", "@types/jest": "^27.0.0", diff --git a/packages/tracing/src/Tracer.ts b/packages/tracing/src/Tracer.ts index ae1bb434bf..849d8328cb 100644 --- a/packages/tracing/src/Tracer.ts +++ b/packages/tracing/src/Tracer.ts @@ -5,6 +5,85 @@ import { HandlerMethodDecorator, TracerOptions, MethodDecorator } from '../types import { ProviderService, ProviderServiceInterface } from './provider'; import { Segment, Subsegment } from 'aws-xray-sdk-core'; +/** + * ## Intro + * Tracer is an opinionated thin wrapper for [AWS X-Ray SDK for Node.js](https://github.com/aws/aws-xray-sdk-node). + * + * Tracing data can be visualized through AWS X-Ray Console. + * + * ## Key features + * * Auto capture cold start as annotation, and responses or full exceptions as metadata + * * Auto-disable when not running in AWS Lambda environment + * * Support tracing functions via decorators, middleware, and manual instrumentation + * * Support tracing AWS SDK v2 and v3 via AWS X-Ray SDK for Node.js + * + * ## Usage + * + * ### Functions usage with middlewares + * TBD + * + * ### Object oriented usage with decorators + * + * If you use TypeScript Classes to wrap your Lambda handler you can use the [@tracer.captureLambdaHanlder()](./_aws_lambda_powertools_tracer.Tracer.html#captureLambdaHanlder) decorator to automatically: + * * handle the subsegment lifecycle + * * add the `ColdStart` annotation + * * add the function response as metadata + * * add the function error as metadata (if any) + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * // FYI: Decorator might not render properly in VSCode mouse over due to https://github.com/microsoft/TypeScript/issues/39371 and might show as *@tracer* instead of `@tracer.captureLambdaHanlder` + * + * class Lambda { + * @tracer.captureLambdaHanlder() + * public handler(event: any, context: any) { + * ... + * } + * } + * + * export const handlerClass = new Lambda(); + * export const handler = handlerClass.handler; + * ``` + * + * ### Functions usage with manual instrumentation + * + * If you prefer to manually instrument your Lambda handler you can use the methods in the tracer class directly. + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * import { Segment } from 'aws-xray-sdk-core'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * export const handler = async (_event: any, context: any) => { + * // Create subsegment & set it as active + * const subsegment = new Subsegment(`## ${context.functionName}`); + * tracer.setSegment(subsegment); + * // Add the ColdStart annotation + * this.putAnnotation('ColdStart', tracer.coldStart); + * + * let res; + * try { + * res = await someLogic(); // Do something + * // Add the response as metadata + * tracer.putMetadata(`${context.functionName} response`, data); + * } catch (err) { + * // Add the error as metadata + * subsegment.addError(err, false); + * } + * + * // Close subsegment + * subsegment.close(); + * + * return res; + * } + * ``` + */ class Tracer implements TracerInterface { public static coldStart: boolean = true; @@ -27,24 +106,125 @@ class Tracer implements TracerInterface { this.provider = new ProviderService(); } + /** + * Patch all AWS SDK v2 clients and create traces when your application makes calls to AWS services. + * + * If you want to patch a specific client use {@link captureAWSClient} and if you are using AWS SDK v3 use {@link captureAWSv3Client} instead. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * const AWS = tracer.captureAWS(require('aws-sdk')); + * + * export const handler = async (_event: any, _context: any) => { + * ... + * } + * ``` + * + * @param aws - AWS SDK v2 import + * @returns AWS - Instrumented AWS SDK + */ public captureAWS(aws: T): T { if (this.tracingEnabled === false) return aws; return this.provider.captureAWS(aws); } + /** + * Patch a specific AWS SDK v2 client and create traces when your application makes calls to that AWS service. + * + * If you want to patch all clients use {@link captureAWS} and if you are using AWS SDK v3 use {@link captureAWSv3Client} instead. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html + * + * @example + * ```typescript + * import { S3 } from "aws-sdk"; + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * tracer.captureAWS(require('aws-sdk')); + * const s3 = tracer.captureAWSClient(new S3({ apiVersion: "2006-03-01" })); + * + * export const handler = async (_event: any, _context: any) => { + * ... + * } + * ``` + * + * @param service - AWS SDK v2 client + * @returns service - Instrumented AWS SDK v2 client + */ public captureAWSClient(service: T): T { if (this.tracingEnabled === false) return service; return this.provider.captureAWSClient(service); } + /** + * Patch an AWS SDK v3 client and create traces when your application makes calls to that AWS service. + * + * If you are using AWS SDK v2 use {@link captureAWSClient} instead. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html + * + * @example + * ```typescript + * import { S3Client } from "@aws-sdk/client-s3"; + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * const client = new S3Client({}); + * tracer.captureAWSv3Client(client); + * + * export const handler = async (_event: any, _context: any) => { + * ... + * } + * ``` + * + * @param service - AWS SDK v3 client + * @returns service - Instrumented AWS SDK v3 client + */ public captureAWSv3Client(service: T): T { if (this.tracingEnabled === false) return service; return this.provider.captureAWSv3Client(service); } + /** + * A decorator automating capture of metadata and annotations on segments or subsegments for a Lambda Handler. + * + * Using this decorator on your handler function will automatically: + * * handle the subsegment lifecycle + * * add the `ColdStart` annotation + * * add the function response as metadata + * * add the function error as metadata (if any) + * + * Note: Currently TypeScript only supports decorators on classes and methods. If you are using the + * function syntax, you should use the middleware instead. + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * class Lambda { + * @tracer.captureLambdaHanlder() + * public handler(event: any, context: any) { + * ... + * } + * } + * + * export const handlerClass = new Lambda(); + * export const handler = handlerClass.handler; + * ``` + * + * @decorator Class + */ public captureLambdaHanlder(): HandlerMethodDecorator { return (target, _propertyKey, descriptor) => { const originalMethod = descriptor.value; @@ -76,6 +256,41 @@ class Tracer implements TracerInterface { }; } + /** + * A decorator automating capture of metadata and annotations on segments or subsegments for an arbitrary function. + * + * Using this decorator on your function will automatically: + * * handle the subsegment lifecycle + * * add the function response as metadata + * * add the function error as metadata (if any) + * + * Note: Currently TypeScript only supports decorators on classes and methods. If you are using the + * function syntax, you should use the middleware instead. + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * class Lambda { + * @tracer.captureMethod() + * public myMethod(param: any) { + * ... + * } + * + * public handler(event: any, context: any) { + * ... + * } + * } + * + * export const handlerClass = new Lambda(); + * export const myMethod = handlerClass.myMethod; + * export const handler = handlerClass.handler; + * ``` + * + * @decorator Class + */ public captureMethod(): MethodDecorator { return (target, _propertyKey, descriptor) => { const originalMethod = descriptor.value; @@ -106,6 +321,28 @@ class Tracer implements TracerInterface { }; } + /** + * Get the active segment or subsegment in the current scope. + * + * Usually you won't need to call this method unless you are manipulating segments using the escape hatch pattern. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-segments + * @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/tracer/#escape-hatch-mechanism + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * export const handler = async (_event: any, _context: any) => { + * const currentSegment = tracer.getSegment(); + * ... // Do something with segment + * } + * ``` + * + * @returns segment - The active segment or subsegment in the current scope. + */ public getSegment(): Segment | Subsegment { const segment = this.provider.getSegment(); if (segment === undefined) { @@ -115,6 +352,17 @@ class Tracer implements TracerInterface { return segment; } + /** + * Retrieve the current value of `ColdStart`. + * + * If Tracer has been initialized outside of the Lambda handler then the same instance + * of Tracer will be reused throghout the lifecycle of that same Lambda execution environment + * and this method will return `false` after the first invocation. + * + * @see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html + * + * @returns boolean - true if is cold start otherwise false + */ public static isColdStart(): boolean { if (Tracer.coldStart === true) { Tracer.coldStart = false; @@ -125,6 +373,25 @@ class Tracer implements TracerInterface { return false; } + /** + * Adds annotation to existing segment or subsegment. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-segment.html#xray-sdk-nodejs-segment-annotations + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * export const handler = async (_event: any, _context: any) => { + * tracer.putAnnotation('PaymentStatus', "SUCCESS"); + * } + * ``` + * + * @param key - Annotation key + * @param value - Value for annotation + */ public putAnnotation(key: string, value: string | number | boolean): void { if (this.tracingEnabled === false) return; @@ -137,6 +404,27 @@ class Tracer implements TracerInterface { document?.addAnnotation(key, value); } + /** + * Adds metadata to existing segment or subsegment. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-segment.html#xray-sdk-nodejs-segment-metadata + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * export const handler = async (_event: any, _context: any) => { + * const res = someLogic(); + * tracer.putMetadata('PaymentResponse', res); + * } + * ``` + * + * @param key - Metadata key + * @param value - Value for metadata + * @param timestamp - Namespace that metadata will lie under, if none is passed it will use the serviceName + */ public putMetadata(key: string, value: unknown, namespace?: string | undefined): void { if (this.tracingEnabled === false) return; @@ -151,10 +439,40 @@ class Tracer implements TracerInterface { document?.addMetadata(key, value, namespace); } + /** + * Sets the passed subsegment as the current active subsegment. + * + * If you are using a middleware or a decorator this is done automatically for you. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html + * + * @example + * ```typescript + * import { Tracer } from '@aws-lambda-powertools/tracer'; + * import { Segment } from 'aws-xray-sdk-core'; + * + * const tracer = new Tracer({ serviceName: 'my-service' }); + * + * export const handler = async (_event: any, _context: any) => { + * const subsegment = new Subsegment('### foo.bar'); + * tracer.setSegment(subsegment); + * } + * ``` + * + * @param segment - Subsegment to set as the current segment + */ public setSegment(segment: Segment | Subsegment): void { return this.provider.setSegment(segment); } + /** + * Add an error to the current segment or subsegment as metadata. + * Used internally by decoratorators and middlewares. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-errors + * + * @param error - Error to serialize as metadata + */ private addErrorAsMetadata(error: Error): void { const subsegment = this.getSegment(); if (this.captureError === false) { @@ -166,6 +484,15 @@ class Tracer implements TracerInterface { subsegment.addError(error, false); } + /** + * Add an data to the current segment or subsegment as metadata. + * Used internally by decoratorators and middlewares. + * + * @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-errors + * + * @param data - Data to serialize as metadata + * @param methodName - Name of the method that is being traced + */ private addResponseAsMetadata(data?: unknown, methodName?: string): void { if (data === undefined || this.captureResponse === false || this.tracingEnabled === false) { return; @@ -174,32 +501,62 @@ class Tracer implements TracerInterface { this.putMetadata(`${methodName} response`, data); } + /** + * Add ColdStart annotation to the current segment or subsegment. + * Used internally by decoratorators and middlewares. + */ private annotateColdStart(): void { if (Tracer.isColdStart()) { this.putAnnotation('ColdStart', true); } } + /** + * Getter for `customConfigService`. + * Used internally during initialization. + */ private getCustomConfigService(): ConfigServiceInterface | undefined { return this.customConfigService; } + /** + * Getter for `envVarsService`. + * Used internally during initialization. + */ private getEnvVarsService(): EnvironmentVariablesService { return this.envVarsService; } + /** + * Determine if we are running in a Lambda execution environment. + * Used internally during initialization. + */ private isLambdaExecutionEnv(): boolean { return this.getEnvVarsService()?.getAwsExecutionEnv() !== ''; } + /** + * Determine if we are running inside a SAM CLI process. + * Used internally during initialization. + */ private isLambdaSamCli(): boolean { return this.getEnvVarsService()?.getSamLocal() !== ''; } + /** + * Validate that the service name provided is valid. + * Used internally during initialization. + * + * @param serviceName - Service name to validate + */ private isValidServiceName(serviceName?: string): boolean { return typeof serviceName === 'string' && serviceName.trim().length > 0; } + /** + * Setter for `captureError` based on configuration passed and environment variables. + * Used internally during initialization. + */ private setCaptureError(): void { const customConfigValue = this.getCustomConfigService()?.getTracingCaptureError(); if (customConfigValue !== undefined && customConfigValue.toLowerCase() === 'false') { @@ -216,6 +573,10 @@ class Tracer implements TracerInterface { } } + /** + * Setter for `captureResponse` based on configuration passed and environment variables. + * Used internally during initialization. + */ private setCaptureResponse(): void { const customConfigValue = this.getCustomConfigService()?.getTracingCaptureResponse(); if (customConfigValue !== undefined && customConfigValue.toLowerCase() === 'false') { @@ -232,14 +593,30 @@ class Tracer implements TracerInterface { } } + /** + * Setter for `customConfigService` based on configuration passed. + * Used internally during initialization. + * + * @param customConfigService - Custom configuration service to use + */ private setCustomConfigService(customConfigService?: ConfigServiceInterface): void { this.customConfigService = customConfigService ? customConfigService : undefined; } + /** + * Setter and initializer for `envVarsService`. + * Used internally during initialization. + */ private setEnvVarsService(): void { this.envVarsService = new EnvironmentVariablesService(); } + /** + * Method that reconciles the configuration passed with the environment variables. + * Used internally during initialization. + * + * @param options - Configuration passed to the tracer + */ private setOptions(options: TracerOptions): Tracer { const { enabled, @@ -257,6 +634,12 @@ class Tracer implements TracerInterface { return this; } + /** + * Setter for `customConfigService` based on configurations passed and environment variables. + * Used internally during initialization. + * + * @param serviceName - Name of the service to use + */ private setServiceName(serviceName?: string): void { if (serviceName !== undefined && this.isValidServiceName(serviceName)) { this.serviceName = serviceName; @@ -279,6 +662,12 @@ class Tracer implements TracerInterface { } } + /** + * Setter for `tracingEnabled` based on configurations passed and environment variables. + * Used internally during initialization. + * + * @param enabled - Whether or not tracing is enabled + */ private setTracingEnabled(enabled?: boolean): void { if (enabled !== undefined && enabled === false) { this.tracingEnabled = enabled; diff --git a/packages/tracing/tsconfig.json b/packages/tracing/tsconfig.json index 30559ec1d7..d28abaa6d5 100644 --- a/packages/tracing/tsconfig.json +++ b/packages/tracing/tsconfig.json @@ -7,6 +7,7 @@ "declaration": true, "declarationMap": true, "outDir": "lib", + "removeComments": false, "strict": true, "inlineSourceMap": true, "moduleResolution": "node", diff --git a/packages/tracing/types/Tracer.ts b/packages/tracing/types/Tracer.ts index 7ff38ee1c4..74f966a0f9 100644 --- a/packages/tracing/types/Tracer.ts +++ b/packages/tracing/types/Tracer.ts @@ -2,6 +2,22 @@ import { ConfigServiceInterface } from '../src/config'; import { Handler } from 'aws-lambda'; import { LambdaInterface } from '../examples/utils/lambda'; +/** + * Options for the tracer class to be used during initialization. + * + * Usage: + * @example + * ```typescript + * const customConfigService: ConfigServiceInterface; + * const tracerOptions: TracerOptions = { + * enabled?: true, + * serviceName?: 'my-service', + * customConfigService?: customConfigService, // Only needed for advanced uses + * }; + * + * const tracer = new Tracer(tracerOptions); + * ``` + */ type TracerOptions = { enabled?: boolean serviceName?: string