UNPKG

@aws-lambda-powertools/metrics

Version:
944 lines (943 loc) 39.8 kB
import { Console } from 'node:console'; import { isIntegerNumber, isNumber, isString, isStringUndefinedNullEmpty, Utility, } from '@aws-lambda-powertools/commons'; import { getBooleanFromEnv, getServiceName, getStringFromEnv, isDevMode, } from '@aws-lambda-powertools/commons/utils/env'; import { COLD_START_METRIC, DEFAULT_NAMESPACE, EMF_MAX_TIMESTAMP_FUTURE_AGE, EMF_MAX_TIMESTAMP_PAST_AGE, MAX_DIMENSION_COUNT, MAX_METRIC_NAME_LENGTH, MAX_METRIC_VALUES_SIZE, MAX_METRICS_SIZE, MetricResolution as MetricResolutions, MetricUnit as MetricUnits, MIN_METRIC_NAME_LENGTH, } from './constants.js'; import { DimensionsStore } from './DimensionsStore.js'; import { MetadataStore } from './MetadataStore.js'; import { MetricsStore } from './MetricsStore.js'; /** * The Metrics utility creates custom metrics asynchronously by logging metrics to standard output following {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html | Amazon CloudWatch Embedded Metric Format (EMF)}. * * These metrics can be visualized through Amazon CloudWatch Console. * * **Key features** * - Aggregating up to 100 metrics using a single CloudWatch EMF object (large JSON blob). * - Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics). * - Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency. * - Creating a one-off metric with different dimensions. * * After initializing the Metrics class, you can add metrics using the {@link Metrics.addMetric | `addMetric()`} method. * The metrics are stored in a buffer and are flushed when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}. * Each metric can have dimensions and metadata added to it. * * @example * ```typescript * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders', * defaultDimensions: { environment: 'dev' }, * }); * * export const handler = async (event: { requestId: string }) => { * metrics.addMetadata('request_id', event.requestId); * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * metrics.publishStoredMetrics(); * }; * ``` * * If you don't want to manually flush the metrics, you can use the {@link Metrics.logMetrics | `logMetrics()`} decorator or * the Middy.js middleware to automatically flush the metrics after the handler function returns or throws an error. * * In addition to this, the decorator and middleware can also be configured to capture a `ColdStart` metric and * set default dimensions for all metrics. * * **Class method decorator** * * @example * * ```typescript * import type { Context } from 'aws-lambda'; * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * class Lambda implements LambdaInterface { * ⁣@metrics.logMetrics({ captureColdStartMetric: true, throwOnEmptyMetrics: true }) * public async handler(_event: { requestId: string }, _: Context) { * metrics.addMetadata('request_id', event.requestId); * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * } * } * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * Note that decorators are a Stage 3 proposal for JavaScript and are not yet part of the ECMAScript standard. * The current implmementation in this library is based on the legacy TypeScript decorator syntax enabled by the [`experimentalDecorators` flag](https://www.typescriptlang.org/tsconfig/#experimentalDecorators) * set to `true` in the `tsconfig.json` file. * * **Middy.js middleware** * * @example * * ```typescript * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * import { logMetrics } from '@aws-lambda-powertools/metrics/middleware'; * import middy from '@middy/core'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = middy(async () => { * metrics.addMetadata('request_id', event.requestId); * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * }).use(logMetrics(metrics, { * captureColdStartMetric: true, * throwOnEmptyMetrics: true, * })); * ``` * * The `logMetrics()` middleware is compatible with `@middy/core@3.x` and above. * */ class Metrics extends Utility { /** * Console instance used to print logs. * * In AWS Lambda, we create a new instance of the Console class so that we can have * full control over the output of the logs. In testing environments, we use the * default console instance. * * This property is initialized in the constructor in setOptions(). * * @private */ console; /** * Custom configuration service for metrics */ customConfigService; /** * Storage for dimensions */ #dimensionsStore = new DimensionsStore(); /** * Name of the Lambda function */ functionName; /** * Custom logger object used for emitting debug, warning, and error messages. * * Note that this logger is not used for emitting metrics which are emitted to standard output using the `Console` object. */ #logger; /** * Flag indicating if this is a single metric instance * @default false */ isSingleMetric = false; /** * Additional metadata to be included with metrics */ #metadataStore = new MetadataStore(); /** * Namespace for the metrics */ namespace; /** * Flag to determine if an error should be thrown when no metrics are recorded * @default false */ shouldThrowOnEmptyMetrics = false; /** * Storage for metrics before they are published */ #metricsStore = new MetricsStore(); /** * Whether to disable metrics */ disabled = false; /** * Cached environment config values. * Initialized once in setEnvConfig(). */ #envConfig = { namespace: '', functionName: '', serviceName: '', disabled: false, devMode: false, }; constructor(options = {}) { super(); this.setEnvConfig(); this.setConsole(); this.#logger = options.logger || this.console; this.setOptions(options); } /** * Add a dimension to metrics. * * A dimension is a key-value pair that is used to group metrics, and it is included in all metrics emitted after it is added. * Invalid dimension values are skipped and a warning is logged. * * When calling the {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are * included in all metrics, use the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method. * * @param name - The name of the dimension * @param value - The value of the dimension */ addDimension(name, value) { if (isStringUndefinedNullEmpty(name) || isStringUndefinedNullEmpty(value)) { this.#logger.warn(`The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`); return; } if (MAX_DIMENSION_COUNT <= this.#dimensionsStore.getDimensionCount()) { throw new RangeError(`The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}`); } const dimensions = this.#dimensionsStore.getDimensions(); const defaultDimensions = this.#dimensionsStore.getDefaultDimensions(); if (Object.hasOwn(dimensions, name) || Object.hasOwn(defaultDimensions, name)) { this.#logger.warn(`Dimension "${name}" has already been added. The previous value will be overwritten.`); } this.#dimensionsStore.addDimension(name, value); } /** * Add multiple dimensions to the metrics. * * This method is useful when you want to add multiple dimensions to the metrics at once. * Invalid dimension values are skipped and a warning is logged. * * When calling the {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are * included in all metrics, use the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method. * * @param dimensions - An object with key-value pairs of dimensions */ addDimensions(dimensions) { const newDimensions = this.#sanitizeDimensions(dimensions); const currentCount = this.#dimensionsStore.getDimensionCount(); const newSetCount = Object.keys(newDimensions).length; if (currentCount + newSetCount >= MAX_DIMENSION_COUNT) { throw new RangeError(`The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}`); } this.#dimensionsStore.addDimensionSet(newDimensions); } /** * A metadata key-value pair to be included with metrics. * * You can use this method to add high-cardinality data as part of your metrics. * This is useful when you want to search highly contextual information along with your metrics in your logs. * * Note that the metadata is not included in the Amazon CloudWatch UI, but it can be used to search and filter logs. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = async (event) => { * metrics.addMetadata('request_id', event.requestId); * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * metrics.publishStoredMetrics(); * }; * ``` * * @param key - The key of the metadata * @param value - The value of the metadata */ addMetadata(key, value) { this.#metadataStore.set(key, value); } /** * Add a metric to the metrics buffer. * * By default, metrics are buffered and flushed when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, * or at the end of the handler function when using the {@link Metrics.logMetrics | `logMetrics()`} decorator or the Middy.js middleware. * * Metrics are emitted to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. * * You can add a metric by specifying the metric name, unit, and value. For convenience, * we provide a set of constants for the most common units in the {@link MetricUnits | MetricUnit} dictionary object. * * Optionally, you can specify a {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition | resolution}, which can be either `High` or `Standard`, using the {@link MetricResolutions | MetricResolution} dictionary object. * By default, metrics are published with a resolution of `Standard`. * * @example * ```typescript * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = async () => { * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * metrics.publishStoredMetrics(); * }; * ``` * * @param name - The metric name * @param unit - The metric unit, see {@link MetricUnits | MetricUnit} * @param value - The metric value * @param resolution - The metric resolution, see {@link MetricResolutions | MetricResolution} */ addMetric(name, unit, value, resolution = MetricResolutions.Standard) { this.storeMetric(name, unit, value, resolution); if (this.isSingleMetric) this.publishStoredMetrics(); } /** * Immediately emit a `ColdStart` metric if this is a cold start invocation. * * A cold start is when AWS Lambda initializes a new instance of your function. To take advantage of this feature, * you must instantiate the Metrics class outside of the handler function. * * By using this method, the metric will be emitted immediately without you having to call {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}. * * If you are using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, you can enable this * feature by setting the `captureColdStartMetric` option to `true`. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = async () => { * metrics.captureColdStartMetric(); * }; * ``` * * @param functionName - Optional function name to use as `function_name` dimension in the metric. It's used only if the `functionName` constructor parameter or environment variable are not set. */ captureColdStartMetric(functionName) { if (!this.getColdStart()) return; const singleMetric = this.singleMetric(); const value = this.functionName?.trim() ?? functionName?.trim(); if (value && value.length > 0) { singleMetric.addDimension('function_name', value); } singleMetric.addMetric(COLD_START_METRIC, MetricUnits.Count, 1); } /** * Clear all previously set default dimensions. * * This will remove all default dimensions set by the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method * or via the `defaultDimensions` parameter in the constructor. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders', * defaultDimensions: { environment: 'dev' }, * }); * * metrics.setDefaultDimensions({ region: 'us-west-2' }); * * // both environment and region dimensions are removed * metrics.clearDefaultDimensions(); * ``` */ clearDefaultDimensions() { this.#dimensionsStore.clearDefaultDimensions(); } /** * Clear all the dimensions added to the Metrics instance via {@link Metrics.addDimension | `addDimension()`} or {@link Metrics.addDimensions | `addDimensions()`}. * * These dimensions are normally cleared when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}, but * you can use this method to clear specific dimensions that you no longer need at runtime. * * This method does not clear the default dimensions set via {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} or via * the `defaultDimensions` parameter in the constructor. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = async () => { * metrics.addDimension('region', 'us-west-2'); * * // ... * * metrics.clearDimensions(); // olnly the region dimension is removed * }; * ``` * * The method is primarily intended for internal use, but it is exposed for advanced use cases. */ clearDimensions() { this.#dimensionsStore.clearRequestDimensions(); } /** * Clear all the metadata added to the Metrics instance. * * Metadata is normally cleared when calling {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`}, but * you can use this method to clear specific metadata that you no longer need at runtime. * * The method is primarily intended for internal use, but it is exposed for advanced use cases. */ clearMetadata() { this.#metadataStore.clear(); } /** * Clear all the metrics stored in the buffer. * * This is useful when you want to clear the metrics stored in the buffer without publishing them. * * The method is primarily intended for internal use, but it is exposed for advanced use cases. */ clearMetrics() { this.#metricsStore.clearMetrics(); } /** * Check if there are stored metrics in the buffer. */ hasStoredMetrics() { return this.#metricsStore.hasMetrics(); } /** * Whether metrics are disabled. */ isDisabled() { return this.disabled; } /** * A class method decorator to automatically log metrics after the method returns or throws an error. * * The decorator can be used with TypeScript classes and can be configured to optionally capture a `ColdStart` metric (see {@link Metrics.captureColdStartMetric | `captureColdStartMetric()`}), * throw an error if no metrics are emitted (see {@link Metrics.setThrowOnEmptyMetrics | `setThrowOnEmptyMetrics()`}), * and set default dimensions for all metrics (see {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`}). * * @example * * ```typescript * import type { Context } from 'aws-lambda'; * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * class Lambda implements LambdaInterface { * ⁣@metrics.logMetrics({ captureColdStartMetric: true }) * public async handler(_event: { requestId: string }, _: Context) { * metrics.addMetadata('request_id', event.requestId); * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * } * } * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * You can configure the decorator with the following options: * - `captureColdStartMetric` - Whether to capture a `ColdStart` metric * - `defaultDimensions` - Default dimensions to add to all metrics * - `throwOnEmptyMetrics` - Whether to throw an error if no metrics are emitted * * @param options - Options to configure the behavior of the decorator, see {@link ExtraOptions} */ logMetrics(options = {}) { const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (throwOnEmptyMetrics) { this.setThrowOnEmptyMetrics(throwOnEmptyMetrics); } if (defaultDimensions !== undefined) { this.setDefaultDimensions(defaultDimensions); } return (_target, _propertyKey, descriptor) => { // biome-ignore lint/style/noNonNullAssertion: The descriptor.value is the method this decorator decorates, it cannot be undefined. const originalMethod = descriptor.value; const metricsRef = this; // Use a function() {} instead of an () => {} arrow function so that we can // access `myClass` as `this` in a decorated `myClass.myMethod()`. descriptor.value = async function (...args) { if (captureColdStartMetric) { metricsRef.captureColdStartMetric(args[1].functionName); } let result; try { result = await originalMethod.apply(this, args); } finally { metricsRef.publishStoredMetrics(); } return result; }; return descriptor; }; } /** * Flush the stored metrics to standard output. * * The method empties the metrics buffer and emits the metrics to standard output in the Amazon CloudWatch EMF (Embedded Metric Format) schema. * * When using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the metrics are automatically flushed after the handler function returns or throws an error. * * @example * ```typescript * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = async () => { * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * metrics.publishStoredMetrics(); * }; * ``` */ publishStoredMetrics() { const hasMetrics = this.hasStoredMetrics(); if (!this.shouldThrowOnEmptyMetrics && !hasMetrics) { this.#logger.warn('No application metrics to publish. The cold-start metric may be published if enabled. ' + 'If application metrics should never be empty, consider using `throwOnEmptyMetrics`'); } /* v8 ignore else -- @preserve */ if (!this.disabled) { const emfOutput = this.serializeMetrics(); hasMetrics && this.console.log(JSON.stringify(emfOutput)); } this.clearMetrics(); this.clearDimensions(); this.clearMetadata(); } /** * Sets the timestamp for the metric. * * If an integer is provided, it is assumed to be the epoch time in milliseconds. * If a Date object is provided, it will be converted to epoch time in milliseconds. * * The timestamp must be a Date object or an integer representing an epoch time. * This should not exceed 14 days in the past or be more than 2 hours in the future. * Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch. * * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html * * @example * ```typescript * import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders', * }); * * export const handler = async () => { * const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago * metrics.setTimestamp(metricTimestamp); * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * }; * ``` * @param timestamp - The timestamp to set, which can be a number or a Date object. */ setTimestamp(timestamp) { if (!this.#validateEmfTimestamp(timestamp)) { this.#logger.warn("This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + 'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.'); } this.#metricsStore.setTimestamp(timestamp); } /** * Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema. * * The EMF schema is a JSON object that contains the following properties: * - `_aws`: An object containing the timestamp and the CloudWatch metrics. * - `CloudWatchMetrics`: An array of CloudWatch metrics objects. * - `Namespace`: The namespace of the metrics. * - `Dimensions`: An array of dimensions for the metrics. * - `Metrics`: An array of metric definitions. * * The object is then emitted to standard output, which in AWS Lambda is picked up by CloudWatch logs and processed asynchronously. */ serializeMetrics() { const metricDefinitions = this.#metricsStore .getAllMetrics() .map((metricDefinition) => { return { Name: metricDefinition.name, Unit: metricDefinition.unit, ...(metricDefinition.resolution === MetricResolutions.High ? { StorageResolution: metricDefinition.resolution } : {}), }; }); if (metricDefinitions.length === 0 && this.shouldThrowOnEmptyMetrics) { throw new RangeError('The number of metrics recorded must be higher than zero'); } if (!this.namespace) this.#logger.warn('Namespace should be defined, default used'); // We reduce the stored metrics to a single object with the metric // name as the key and the value as the value. const metricValues = this.#metricsStore.getAllMetrics().reduce((result, { name, value, }) => { result[name] = value; return result; }, {}); const dimensionNames = []; const dimensions = this.#dimensionsStore.getDimensions(); const dimensionSets = this.#dimensionsStore.getDimensionSets(); const defaultDimensions = this.#dimensionsStore.getDefaultDimensions(); const allDimensionKeys = new Set([ ...Object.keys(defaultDimensions), ...Object.keys(dimensions), ]); if (Object.keys(dimensions).length > 0) { dimensionNames.push([...allDimensionKeys]); } for (const dimensionSet of dimensionSets) { const dimensionSetKeys = new Set([ ...Object.keys(defaultDimensions), ...Object.keys(dimensionSet), ]); dimensionNames.push([...dimensionSetKeys]); } if (dimensionNames.length === 0 && Object.keys(defaultDimensions).length > 0) { dimensionNames.push(Object.keys(defaultDimensions)); } return { _aws: { Timestamp: this.#metricsStore.getTimestamp() ?? Date.now(), CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, Dimensions: dimensionNames, Metrics: metricDefinitions, }, ], }, ...defaultDimensions, ...dimensions, // Merge all dimension sets efficiently by mutating the accumulator ...dimensionSets.reduce((acc, dims) => { for (const [key, value] of Object.entries(dims)) { acc[key] = value; } return acc; }, {}), ...metricValues, ...this.#metadataStore.getAll(), }; } /** * Set default dimensions that will be added to all metrics. * * This method will merge the provided dimensions with the existing default dimensions. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders', * defaultDimensions: { environment: 'dev' }, * }); * * // Default dimensions will contain both region and environment * metrics.setDefaultDimensions({ * region: 'us-west-2', * environment: 'prod', * }); * ``` * * @param dimensions - The dimensions to be added to the default dimensions object */ setDefaultDimensions(dimensions) { const newDimensions = this.#sanitizeDimensions(dimensions); const currentDefaultDimensions = this.#dimensionsStore.getDefaultDimensions(); const currentCount = Object.keys(currentDefaultDimensions).length; const newSetCount = Object.keys(newDimensions).length; if (currentCount + newSetCount >= MAX_DIMENSION_COUNT) { throw new RangeError(`The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}`); } this.#dimensionsStore.setDefaultDimensions({ ...currentDefaultDimensions, ...newDimensions, }); } /** * @deprecated Override the function name for `ColdStart` metrics inferred from the context either via: * - `functionName` constructor parameter * - `POWERTOOLS_FUNCTION_NAME` environment variable * - {@link Metrics.captureColdStartMetric | `captureColdStartMetric('myFunctionName')`} method */ /* v8 ignore next -- @preserve */ setFunctionName(name) { this.functionName = name; } /** * Set the flag to throw an error if no metrics are emitted. * * You can use this method to enable or disable this opt-in feature. This is useful if you want to ensure * that at least one metric is emitted when flushing the metrics. This can be useful to catch bugs where * metrics are not being emitted as expected. * * @param enabled - Whether to throw an error if no metrics are emitted */ setThrowOnEmptyMetrics(enabled) { this.shouldThrowOnEmptyMetrics = enabled; } /** * Create a new Metrics instance configured to immediately flush a single metric. * * CloudWatch EMF uses the same dimensions and timestamp across all your metrics, this is useful when you have a metric that should have different dimensions * or when you want to emit a single metric without buffering it. * * This method is used internally by the {@link Metrics.captureColdStartMetric | `captureColdStartMetric()`} method to emit the `ColdStart` metric immediately * after the handler function is called. * * @example * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ * namespace: 'serverlessAirline', * serviceName: 'orders' * }); * * export const handler = async () => { * const singleMetric = metrics.singleMetric(); * // The single metric will be emitted immediately * singleMetric.addMetric('ColdStart', MetricUnit.Count, 1); * * // These other metrics will be buffered and emitted when calling `publishStoredMetrics()` * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * metrics.publishStoredMetrics(); * }; */ singleMetric() { return new Metrics({ namespace: this.namespace, defaultDimensions: this.#dimensionsStore.getDefaultDimensions(), singleMetric: true, logger: this.#logger, }); } /** * @deprecated Use {@link Metrics.setThrowOnEmptyMetrics | `setThrowOnEmptyMetrics()`} instead. */ /* v8 ignore next -- @preserve */ throwOnEmptyMetrics() { this.shouldThrowOnEmptyMetrics = true; } /** * Get the custom config service if it exists. */ getCustomConfigService() { return this.customConfigService; } /** * Initialize the console property as an instance of the internal version of `Console()` class (PR #748) * or as the global node console if the `POWERTOOLS_DEV' env variable is set and has truthy value. * * @private */ setConsole() { if (!this.#envConfig.devMode) { this.console = new Console({ stdout: process.stdout, stderr: process.stderr, }); } else { this.console = console; } } /** * Set the custom config service to be used. * * @param customConfigService The custom config service to be used */ setCustomConfigService(customConfigService) { this.customConfigService = customConfigService ? customConfigService : undefined; } /** * Set the environment variables service to be used. */ setEnvConfig() { this.#envConfig.namespace = getStringFromEnv({ key: 'POWERTOOLS_METRICS_NAMESPACE', defaultValue: '', }); this.#envConfig.functionName = getStringFromEnv({ key: 'POWERTOOLS_METRICS_FUNCTION_NAME', defaultValue: '', }); this.#envConfig.serviceName = getServiceName(); this.#envConfig.disabled = getBooleanFromEnv({ key: 'POWERTOOLS_METRICS_DISABLED', defaultValue: false, extendedParsing: true, }); this.#envConfig.devMode = isDevMode(); } /** * Set the function name for the cold start metric. * * @param functionName - The function name to be used for the cold start metric set in the constructor */ setFunctionNameForColdStartMetric(functionName) { const value = functionName?.trim() ?? this.#envConfig.functionName; if (value && value.length > 0) { this.functionName = value; } } /** * Set the namespace to be used. * * @param namespace - The namespace to be used */ setNamespace(namespace) { this.namespace = namespace || this.getCustomConfigService()?.getNamespace() || this.#envConfig.namespace; } /** * Set the disbaled flag based on the environment variables `POWERTOOLS_METRICS_DISABLED` and `POWERTOOLS_DEV`. * * The `POWERTOOLS_METRICS_DISABLED` environment variable takes precedence over `POWERTOOLS_DEV`. */ setDisabled() { if ('POWERTOOLS_METRICS_DISABLED' in process.env && process.env.POWERTOOLS_METRICS_DISABLED !== undefined) { this.disabled = this.#envConfig.disabled; return; } this.disabled = this.#envConfig.devMode; } /** * Set the options to be used by the Metrics instance. * * This method is used during the initialization of the Metrics instance. * * @param options - The options to be used */ setOptions(options) { const { customConfigService, namespace, serviceName, singleMetric, defaultDimensions, functionName, } = options; this.setCustomConfigService(customConfigService); this.setDisabled(); this.setNamespace(namespace); const resolvedServiceName = this.#resolveServiceName(serviceName); this.setDefaultDimensions(defaultDimensions ? { service: resolvedServiceName, ...defaultDimensions } : { service: resolvedServiceName }); this.setFunctionNameForColdStartMetric(functionName); this.isSingleMetric = singleMetric || false; return this; } /** * Set the service dimension that will be included in the metrics. * * @param service - The service to be used */ #resolveServiceName(service) { return (service || this.getCustomConfigService()?.getServiceName() || this.#envConfig.serviceName || this.defaultServiceName); } /** * Store a metric in the buffer. * * If the buffer is full, or the metric reaches the maximum number of values, * the metrics are flushed to stdout. * * @param name - The name of the metric to store * @param unit - The unit of the metric to store * @param value - The value of the metric to store * @param resolution - The resolution of the metric to store */ storeMetric(name, unit, value, resolution) { if (!isString(name)) throw new Error(`${name} is not a valid string`); if (name.length < MIN_METRIC_NAME_LENGTH || name.length > MAX_METRIC_NAME_LENGTH) throw new RangeError(`The metric name should be between ${MIN_METRIC_NAME_LENGTH} and ${MAX_METRIC_NAME_LENGTH} characters`); if (!isNumber(value)) throw new RangeError(`${value} is not a valid number`); if (!Object.values(MetricUnits).includes(unit)) throw new RangeError(`Invalid metric unit '${unit}', expected either option: ${Object.values(MetricUnits).join(',')}`); if (!Object.values(MetricResolutions).includes(resolution)) throw new RangeError(`Invalid metric resolution '${resolution}', expected either option: ${Object.values(MetricResolutions).join(',')}`); if (this.#metricsStore.getMetricsCount() >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } const storedMetric = this.#metricsStore.setMetric(name, unit, value, resolution); if (Array.isArray(storedMetric.value) && storedMetric.value.length === MAX_METRIC_VALUES_SIZE) { this.publishStoredMetrics(); } } /** * Validates a given timestamp based on CloudWatch Timestamp guidelines. * * Timestamp must meet CloudWatch requirements. * The time stamp can be up to two weeks in the past and up to two hours into the future. * See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) * for valid values. * * @param timestamp - Date object or epoch time in milliseconds representing the timestamp to validate. */ #validateEmfTimestamp(timestamp) { const isDate = timestamp instanceof Date; if (!isDate && !isIntegerNumber(timestamp)) { return false; } const timestampMs = isDate ? timestamp.getTime() : timestamp; const currentTime = Date.now(); const minValidTimestamp = currentTime - EMF_MAX_TIMESTAMP_PAST_AGE; const maxValidTimestamp = currentTime + EMF_MAX_TIMESTAMP_FUTURE_AGE; return timestampMs >= minValidTimestamp && timestampMs <= maxValidTimestamp; } /** * Sanitizes the dimensions by removing invalid entries and skipping duplicates. * * @param dimensions - The dimensions to sanitize. */ #sanitizeDimensions(dimensions) { const newDimensions = {}; const currentDimensions = this.#dimensionsStore.getDimensions(); for (const [key, value] of Object.entries(dimensions)) { if (isStringUndefinedNullEmpty(key) || isStringUndefinedNullEmpty(value)) { this.#logger.warn(`The dimension ${key} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`); continue; } const defaultDimensions = this.#dimensionsStore.getDefaultDimensions(); if (Object.hasOwn(currentDimensions, key) || Object.hasOwn(defaultDimensions, key) || Object.hasOwn(newDimensions, key)) { this.#logger.warn(`Dimension "${key}" has already been added. The previous value will be overwritten.`); } newDimensions[key] = value; } return newDimensions; } } export { Metrics };