@aws-lambda-powertools/metrics
Version:
The metrics package for the AWS Lambda Powertools for TypeScript library
484 lines • 29.6 kB
JavaScript
"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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