UNPKG

@aws-lambda-powertools/metrics

Version:
484 lines 29.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MetricResolution = exports.MetricUnits = exports.Metrics = void 0; const commons_1 = require("@aws-lambda-powertools/commons"); const config_1 = require("./config"); const types_1 = require("./types"); Object.defineProperty(exports, "MetricUnits", { enumerable: true, get: function () { return types_1.MetricUnits; } }); Object.defineProperty(exports, "MetricResolution", { enumerable: true, get: function () { return types_1.MetricResolution; } }); const MAX_METRICS_SIZE = 100; const MAX_DIMENSION_COUNT = 29; const DEFAULT_NAMESPACE = 'default_namespace'; /** * ## Intro * Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF). * * These metrics can be visualized through Amazon CloudWatch Console. * * ## Key features * * Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob) * * Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc) * * Metrics are created asynchronously by CloudWatch service, no custom stacks needed * * Context manager to create a one off metric with a different dimension * * ## Usage * * ### Functions usage with middleware * * Using this middleware on your handler function will automatically flush metrics after the function returns or throws an error. * Additionally, you can configure the middleware to easily: * * ensure that at least one metric is emitted before you flush them * * capture a `ColdStart` a metric * * set default dimensions for all your metrics * * @example * ```typescript * import { Metrics, logMetrics } from '@aws-lambda-powertools/metrics'; * import middy from '@middy/core'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); * * const lambdaHandler = async (_event: any, _context: any) => { * ... * }; * * export const handler = middy(lambdaHandler).use(logMetrics(metrics)); * ``` * * ### Object oriented way with decorator * * If you are used to TypeScript Class usage to encapsulate your Lambda handler you can leverage the [@metrics.logMetrics()](./_aws_lambda_powertools_metrics.Metrics.html#logMetrics) decorator to automatically: * * capture a `ColdStart` metric * * flush buffered metrics * * throw on empty metrics * * @example * * ```typescript * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; * import { LambdaInterface } from '@aws-lambda-powertools/commons'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); * * class Lambda implements LambdaInterface { * * // FYI: Decorator might not render properly in VSCode mouse over due to https://github.com/microsoft/TypeScript/issues/47679 and might show as *@metrics* instead of `@metrics.logMetrics` * * @metrics.logMetrics({ captureColdStartMetric: true, throwOnEmptyMetrics: true }) * public handler(_event: any, _context: any): Promise<void> { * // ... * metrics.addMetric('test-metric', MetricUnits.Count, 10); * // ... * } * } * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * ### Standard function * * If you are used to classic JavaScript functions, you can leverage the different methods provided to create and publish metrics. * * @example * * ```typescript * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); * * export const handler = async (_event: any, _context: any): Promise<void> => { * metrics.captureColdStartMetric(); * metrics.addMetric('test-metric', MetricUnits.Count, 10); * metrics.publishStoredMetrics(); * }; * ``` */ class Metrics extends commons_1.Utility { constructor(options = {}) { super(); this.defaultDimensions = {}; this.dimensions = {}; this.isSingleMetric = false; this.metadata = {}; this.shouldThrowOnEmptyMetrics = false; this.storedMetrics = {}; this.dimensions = {}; this.setOptions(options); } /** * Add a dimension to the metrics. * A dimension is a key-value pair that is used to group metrics. * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Dimension for more details. * @param name * @param value */ addDimension(name, value) { if (MAX_DIMENSION_COUNT <= this.getCurrentDimensionsCount()) { throw new RangeError(`The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}`); } this.dimensions[name] = value; } /** * Add multiple dimensions to the metrics. * @param dimensions */ addDimensions(dimensions) { const newDimensions = { ...this.dimensions }; Object.keys(dimensions).forEach((dimensionName) => { newDimensions[dimensionName] = dimensions[dimensionName]; }); if (Object.keys(newDimensions).length > MAX_DIMENSION_COUNT) { throw new RangeError(`Unable to add ${Object.keys(dimensions).length} dimensions: the number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}`); } this.dimensions = newDimensions; } /** * A high-cardinality data part of your Metrics log. This is useful when you want to search highly contextual information along with your metrics in your logs. * @param key * @param value */ addMetadata(key, value) { this.metadata[key] = value; } /** * Add a metric to the metrics buffer. * * @example * * Add Metric using MetricUnit Enum supported by Cloudwatch * * ```ts * metrics.addMetric('successfulBooking', MetricUnits.Count, 1); * ``` * * @example * * Add Metric using MetricResolution type with resolutions High or Standard supported by cloudwatch * * ```ts * metrics.addMetric('successfulBooking', MetricUnits.Count, 1, MetricResolution.High); * ``` * * @param name - The metric name * @param unit - The metric unit * @param value - The metric value * @param resolution - The metric resolution * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition Amazon Cloudwatch Concepts Documentation * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html#CloudWatch_Embedded_Metric_Format_Specification_structure_metricdefinition Metric Definition of Embedded Metric Format Specification */ addMetric(name, unit, value, resolution = types_1.MetricResolution.Standard) { this.storeMetric(name, unit, value, resolution); if (this.isSingleMetric) this.publishStoredMetrics(); } /** * Create a singleMetric to capture cold start. * If it's a cold start invocation, this feature will: * * Create a separate EMF blob solely containing a metric named ColdStart * * Add function_name and service dimensions * * This has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions. * * @example * * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); * * export const handler = async (event: any, _context: any): Promise<void> => { * metrics.captureColdStartMetric(); * }; * ``` */ captureColdStartMetric() { if (!this.isColdStart()) return; const singleMetric = this.singleMetric(); if (this.defaultDimensions.service) { singleMetric.setDefaultDimensions({ service: this.defaultDimensions.service }); } if (this.functionName != null) { singleMetric.addDimension('function_name', this.functionName); } singleMetric.addMetric('ColdStart', types_1.MetricUnits.Count, 1); } clearDefaultDimensions() { this.defaultDimensions = {}; } clearDimensions() { this.dimensions = {}; } clearMetadata() { this.metadata = {}; } clearMetrics() { this.storedMetrics = {}; } /** * A decorator automating coldstart capture, throw on empty metrics and publishing metrics on handler exit. * * @example * * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * import { LambdaInterface } from '@aws-lambda-powertools/commons'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); * * class Lambda implements LambdaInterface { * * @metrics.logMetrics({ captureColdStartMetric: true }) * public handler(_event: any, _context: any): Promise<void> { * // ... * } * } * * const handlerClass = new Lambda(); * export const handler = handlerClass.handler.bind(handlerClass); * ``` * * @decorator Class */ logMetrics(options = {}) { const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (throwOnEmptyMetrics) { this.throwOnEmptyMetrics(); } if (defaultDimensions !== undefined) { this.setDefaultDimensions(defaultDimensions); } return (_target, _propertyKey, descriptor) => { /** * The descriptor.value is the method this decorator decorates, it cannot be undefined. */ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const originalMethod = descriptor.value; // eslint-disable-next-line @typescript-eslint/no-this-alias 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 (event, context, callback) { metricsRef.functionName = context.functionName; if (captureColdStartMetric) metricsRef.captureColdStartMetric(); let result; try { result = await originalMethod.apply(this, [event, context, callback]); } catch (error) { throw error; } finally { metricsRef.publishStoredMetrics(); } return result; }); return descriptor; }; } /** * Synchronous function to actually publish your metrics. (Not needed if using logMetrics decorator). * It will create a new EMF blob and log it to standard output to be then ingested by Cloudwatch logs and processed automatically for metrics creation. * * @example * * ```typescript * import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName: 'orders' }); // Sets metric namespace, and service as a metric dimension * * export const handler = async (_event: any, _context: any): Promise<void> => { * metrics.addMetric('test-metric', MetricUnits.Count, 10); * metrics.publishStoredMetrics(); * }; * ``` */ publishStoredMetrics() { const target = this.serializeMetrics(); console.log(JSON.stringify(target)); this.clearMetrics(); this.clearDimensions(); this.clearMetadata(); } /** * Function to create the right object compliant with Cloudwatch EMF (Embedded Metric Format). * * * @returns metrics as JSON object compliant EMF Schema Specification * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html for more details */ serializeMetrics() { // For high-resolution metrics, add StorageResolution property // Example: [ { "Name": "metric_name", "Unit": "Count", "StorageResolution": 1 } ] // For standard resolution metrics, don't add StorageResolution property to avoid unnecessary ingestion of data into cloudwatch // Example: [ { "Name": "metric_name", "Unit": "Count"} ] const metricDefinitions = Object.values(this.storedMetrics).map((metricDefinition) => this.isHigh(metricDefinition['resolution']) ? ({ Name: metricDefinition.name, Unit: metricDefinition.unit, StorageResolution: metricDefinition.resolution }) : ({ Name: metricDefinition.name, Unit: metricDefinition.unit, })); if (metricDefinitions.length === 0 && this.shouldThrowOnEmptyMetrics) { throw new RangeError('The number of metrics recorded must be higher than zero'); } if (!this.namespace) console.warn('Namespace should be defined, default used'); const metricValues = Object.values(this.storedMetrics).reduce((result, { name, value }) => { result[name] = value; return result; }, {}); const dimensionNames = [...Object.keys(this.defaultDimensions), ...Object.keys(this.dimensions)]; return { _aws: { Timestamp: new Date().getTime(), CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, Dimensions: [dimensionNames], Metrics: metricDefinitions, }, ], }, ...this.defaultDimensions, ...this.dimensions, ...metricValues, ...this.metadata, }; } setDefaultDimensions(dimensions) { const targetDimensions = { ...this.defaultDimensions, ...dimensions, }; if (MAX_DIMENSION_COUNT <= Object.keys(targetDimensions).length) { throw new Error('Max dimension count hit'); } this.defaultDimensions = targetDimensions; } setFunctionName(value) { this.functionName = value; } /** * CloudWatch EMF uses the same dimensions across all your metrics. Use singleMetric if you have a metric that should have different dimensions. * * You don't need to call publishStoredMetrics() after calling addMetric for a singleMetrics, they will be flushed directly. * * @example * * ```typescript * const singleMetric = metrics.singleMetric(); * singleMetric.addDimension('InnerDimension', 'true'); * singleMetric.addMetric('single-metric', MetricUnits.Percent, 50); * ``` * * @returns the Metrics */ singleMetric() { return new Metrics({ namespace: this.namespace, serviceName: this.dimensions.service, defaultDimensions: this.defaultDimensions, singleMetric: true, }); } /** * Throw an Error if the metrics buffer is empty. * * @example * * ```typescript * import { Metrics } from '@aws-lambda-powertools/metrics'; * * const metrics = new Metrics({ namespace: 'serverlessAirline', serviceName:'orders' }); * * export const handler = async (_event: any, _context: any): Promise<void> => { * metrics.throwOnEmptyMetrics(); * metrics.publishStoredMetrics(); // will throw since no metrics added. * }; * ``` */ throwOnEmptyMetrics() { this.shouldThrowOnEmptyMetrics = true; } getCurrentDimensionsCount() { return Object.keys(this.dimensions).length + Object.keys(this.defaultDimensions).length; } getCustomConfigService() { return this.customConfigService; } getEnvVarsService() { return this.envVarsService; } isHigh(resolution) { return resolution === types_1.MetricResolution.High; } isNewMetric(name, unit) { if (this.storedMetrics[name]) { // Inconsistent units indicates a bug or typos and we want to flag this to users early if (this.storedMetrics[name].unit !== unit) { const currentUnit = this.storedMetrics[name].unit; throw new Error(`Metric "${name}" has already been added with unit "${currentUnit}", but we received unit "${unit}". Did you mean to use metric unit "${currentUnit}"?`); } return false; } else { return true; } } setCustomConfigService(customConfigService) { this.customConfigService = customConfigService ? customConfigService : undefined; } setEnvVarsService() { this.envVarsService = new config_1.EnvironmentVariablesService(); } setNamespace(namespace) { this.namespace = (namespace || this.getCustomConfigService()?.getNamespace() || this.getEnvVarsService().getNamespace()); } setOptions(options) { const { customConfigService, namespace, serviceName, singleMetric, defaultDimensions } = options; this.setEnvVarsService(); this.setCustomConfigService(customConfigService); this.setNamespace(namespace); this.setService(serviceName); this.setDefaultDimensions(defaultDimensions); this.isSingleMetric = singleMetric || false; return this; } setService(service) { const targetService = (service || this.getCustomConfigService()?.getServiceName() || this.getEnvVarsService().getServiceName()) || this.getDefaultServiceName(); if (targetService.length > 0) { this.setDefaultDimensions({ service: targetService }); } } storeMetric(name, unit, value, resolution) { if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } if (this.isNewMetric(name, unit)) { this.storedMetrics[name] = { unit, value, name, resolution }; } else { const storedMetric = this.storedMetrics[name]; if (!Array.isArray(storedMetric.value)) { storedMetric.value = [storedMetric.value]; } storedMetric.value.push(value); } } } exports.Metrics = Metrics; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWV0cmljcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9NZXRyaWNzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUNBLDREQUF5RDtBQUV6RCxxQ0FBK0U7QUFDL0UsbUNBWWlCO0FBMGhCZiw0RkE5aEJBLG1CQUFXLE9BOGhCQTtBQUNYLGlHQTloQkEsd0JBQWdCLE9BOGhCQTtBQXpoQmxCLE1BQU0sZ0JBQWdCLEdBQUcsR0FBRyxDQUFDO0FBQzdCLE1BQU0sbUJBQW1CLEdBQUcsRUFBRSxDQUFDO0FBQy9CLE1BQU0saUJBQWlCLEdBQUcsbUJBQW1CLENBQUM7QUFFOUM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQW9GRztBQUNILE1BQU0sT0FBUSxTQUFRLGlCQUFPO0lBWTNCLFlBQW1CLFVBQTBCLEVBQUU7UUFDN0MsS0FBSyxFQUFFLENBQUM7UUFYRixzQkFBaUIsR0FBZSxFQUFFLENBQUM7UUFDbkMsZUFBVSxHQUFlLEVBQUUsQ0FBQztRQUc1QixtQkFBYyxHQUFZLEtBQUssQ0FBQztRQUNoQyxhQUFRLEdBQThCLEVBQUUsQ0FBQztRQUV6Qyw4QkFBeUIsR0FBWSxLQUFLLENBQUM7UUFDM0Msa0JBQWEsR0FBa0IsRUFBRSxDQUFDO1FBS3hDLElBQUksQ0FBQyxVQUFVLEdBQUcsRUFBRSxDQUFDO1FBQ3JCLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDM0IsQ0FBQztJQUVEOzs7Ozs7T0FNRztJQUNJLFlBQVksQ0FBQyxJQUFZLEVBQUUsS0FBYTtRQUM3QyxJQUFJLG1CQUFtQixJQUFJLElBQUksQ0FBQyx5QkFBeUIsRUFBRSxFQUFFO1lBQzNELE1BQU0sSUFBSSxVQUFVLENBQUMsc0RBQXNELG1CQUFtQixFQUFFLENBQUMsQ0FBQztTQUNuRztRQUNELElBQUksQ0FBQyxVQUFVLENBQUMs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