diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 42bc929ebe..f2d61eabf9 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -66,11 +66,12 @@ The library requires two settings. You can set them as environment variables, or These settings will be used across all metrics emitted: -| Setting | Description | Environment variable | Default | Allowed Values | Example | Constructor parameter | -| -------------------- | ---------------------------------------------------------------- | ------------------------------ | ------------------- | -------------- | ------------------- | --------------------- | -| **Service** | Optionally, sets **service** metric dimension across all metrics | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline` | `serviceName` | -| **Metric namespace** | Logical container where all metrics will be placed | `POWERTOOLS_METRICS_NAMESPACE` | `default_namespace` | Any string | `serverlessAirline` | `default_namespace` | -| **Enabled** | Whether to emit metrics to standard output or not | `POWERTOOLS_METRICS_ENABLED` | `true` | Boolean | `false` | | +| Setting | Description | Environment variable | Default | Allowed Values | Example | Constructor parameter | +|----------------------|------------------------------------------------------------------|------------------------------------|----------------------------------------------------------|----------------|---------------------|-----------------------| +| **Service** | Optionally, sets **service** metric dimension across all metrics | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline` | `serviceName` | +| **Metric namespace** | Logical container where all metrics will be placed | `POWERTOOLS_METRICS_NAMESPACE` | `default_namespace` | Any string | `serverlessAirline` | `default_namespace` | +| **Function Name** | Function name used as dimension for the `ColdStart` metric | `POWERTOOLS_METRICS_FUNCTION_NAME` | [See docs](#capturing-a-cold-start-invocation-as-metric) | Any string | `my-function-name` | `functionName` | +| **Enabled** | Whether to emit metrics to standard output or not | `POWERTOOLS_METRICS_ENABLED` | `true` | Boolean | `false` | | !!! tip Use your application name or main service as the metric namespace to easily group all metrics @@ -87,7 +88,7 @@ The `Metrics` utility is instantiated outside of the Lambda handler. In doing th === "template.yml" - ```yaml hl_lines="9 10" + ```yaml hl_lines="8-10" Resources: HelloWorldFunction: Type: AWS::Serverless::Function @@ -97,6 +98,7 @@ The `Metrics` utility is instantiated outside of the Lambda handler. In doing th Variables: POWERTOOLS_SERVICE_NAME: orders POWERTOOLS_METRICS_NAMESPACE: serverlessAirline + POWERTOOLS_METRICS_FUNCTION_NAME: my-function-name ``` You can initialize Metrics anywhere in your code - It'll keep track of your aggregate metrics in memory. @@ -184,7 +186,7 @@ You can call `addMetric()` with the same name multiple times. The values will be ### Adding default dimensions -You can add default dimensions to your metrics by passing them as parameters in 4 ways: +You can add default dimensions to your metrics by passing them as parameters in 4 ways: * in the constructor * in the [Middy-compatible](https://github.com/middyjs/middy){target=_blank} middleware @@ -405,6 +407,28 @@ This has the advantage of keeping cold start metric separate from your applicati !!! info "We do not emit 0 as a value for the ColdStart metric for cost-efficiency reasons. [Let us know](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=) if you'd prefer a flag to override it." +#### Setting function name + +When emitting cold start metrics, the `function_name` dimension defaults to `context.functionName`. If you want to change the value you can set the `functionName` parameter in the metrics constructor, define the environment variable `POWERTOOLS_METRICS_FUNCTION_NAME`, or pass a value to `captureColdStartMetric`. + +The priority of the `function_name` dimension value is defined as: + +1. `functionName` constructor option +2. `POWERTOOLS_METRICS_FUNCTION_NAME` environment variable +3. The value passed in the `captureColdStartMetric` call, or `context.functionName` if using logMetrics decorator or Middy middleware + +=== "constructor" + + ```typescript hl_lines="6" + --8<-- "examples/snippets/metrics/functionName.ts" + ``` + +=== "captureColdStartMetric method" + + ```typescript hl_lines="12" + --8<-- "examples/snippets/metrics/captureColdStartMetric.ts" + ``` + ## Advanced ### Adding metadata diff --git a/docs/index.md b/docs/index.md index 680d346464..cb6256f2ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -351,22 +351,23 @@ Core utilities such as Tracing, Logging, and Metrics will be available across al ???+ info Explicit parameters take precedence over environment variables -| Environment variable | Description | Utility | Default | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------- | -| **POWERTOOLS_SERVICE_NAME** | Set service name used for tracing namespace, metrics dimension and structured logging | All | `service_undefined` | -| **POWERTOOLS_METRICS_NAMESPACE** | Set namespace used for metrics | [Metrics](core/metrics.md) | `default_namespace` | -| **POWERTOOLS_METRICS_ENABLED** | Explicitly disables emitting metrics to stdout | [Metrics](core/metrics.md) | `true` | -| **POWERTOOLS_TRACE_ENABLED** | Explicitly disables tracing | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Capture Lambda or method return as metadata. | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Capture Lambda or method exception as metadata. | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS** | Capture HTTP(s) requests as segments. | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_LOGGER_LOG_EVENT** | Log incoming event | [Logger](core/logger.md) | `false` | -| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logger](core/logger.md) | `0` | -| **POWERTOOLS_DEV** | Increase JSON indentation to ease debugging when running functions locally or in a non-production environment | [Logger](core/logger.md) | `false` | -| **POWERTOOLS_LOG_LEVEL** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | [Logger](core/logger.md) | `INFO` | -| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](utilities/parameters.md) | `5` | -| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Set whether to decrypt or not values retrieved from AWS Systems Manager Parameters Store | [Parameters](utilities/parameters.md) | `false` | -| **POWERTOOLS_IDEMPOTENCY_DISABLED** | Disable the Idempotency logic without changing your code, useful for testing | [Idempotency](utilities/idempotency.md) | `false` | +| Environment variable | Description | Utility | Default | +| -------------------------------------------- |------------------------------------------------------------------------------------------| --------------------------------------- |-------------------------------------------------| +| **POWERTOOLS_SERVICE_NAME** | Set service name used for tracing namespace, metrics dimension and structured logging | All | `service_undefined` | +| **POWERTOOLS_METRICS_NAMESPACE** | Set namespace used for metrics | [Metrics](core/metrics.md) | `default_namespace` | +| **POWERTOOLS_METRICS_FUNCTION_NAME** | Function name used as dimension for the `ColdStart` metric | [Metrics](core/metrics.md) | [See docs](core/metrics/#setting-function-name) | +| **POWERTOOLS_METRICS_ENABLED** | Explicitly disables emitting metrics to stdout | [Metrics](core/metrics.md) | `true` | +| **POWERTOOLS_TRACE_ENABLED** | Explicitly disables tracing | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Capture Lambda or method return as metadata. | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Capture Lambda or method exception as metadata. | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS** | Capture HTTP(s) requests as segments. | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_LOGGER_LOG_EVENT** | Log incoming event | [Logger](core/logger.md) | `false` | +| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logger](core/logger.md) | `0` | +| **POWERTOOLS_DEV** | Pretty-print logs, disable metrics flushing, and disable traces - use for dev only | See section below | `false` | +| **POWERTOOLS_LOG_LEVEL** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | [Logger](core/logger.md) | `INFO` | +| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](utilities/parameters.md) | `5` | +| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Set whether to decrypt or not values retrieved from AWS Systems Manager Parameters Store | [Parameters](utilities/parameters.md) | `false` | +| **POWERTOOLS_IDEMPOTENCY_DISABLED** | Disable the Idempotency logic without changing your code, useful for testing | [Idempotency](utilities/idempotency.md) | `false` | Each Utility page provides information on example values and allowed values. diff --git a/examples/snippets/metrics/captureColdStartMetric.ts b/examples/snippets/metrics/captureColdStartMetric.ts new file mode 100644 index 0000000000..289934f9a7 --- /dev/null +++ b/examples/snippets/metrics/captureColdStartMetric.ts @@ -0,0 +1,17 @@ +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', +}); + +export const handler = async ( + _event: unknown, + _context: unknown +): Promise => { + metrics.captureColdStartMetric('my-function-name'); + + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + + metrics.publishStoredMetrics(); +}; diff --git a/examples/snippets/metrics/functionName.ts b/examples/snippets/metrics/functionName.ts new file mode 100644 index 0000000000..2811a45af1 --- /dev/null +++ b/examples/snippets/metrics/functionName.ts @@ -0,0 +1,18 @@ +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', + functionName: 'my-function-name', +}); + +export const handler = async ( + _event: unknown, + _context: unknown +): Promise => { + metrics.captureColdStartMetric(); + + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + + metrics.publishStoredMetrics(); +}; diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 93b2e35787..a3a743e96d 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -371,8 +371,10 @@ class Metrics extends Utility implements MetricsInterface { * 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. */ - public captureColdStartMetric(): void { + public captureColdStartMetric(functionName?: string): void { if (!this.getColdStart()) return; const singleMetric = this.singleMetric(); @@ -381,8 +383,9 @@ class Metrics extends Utility implements MetricsInterface { service: this.defaultDimensions.service, }); } - if (this.functionName != null) { - singleMetric.addDimension('function_name', this.functionName); + 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); } @@ -543,8 +546,9 @@ class Metrics extends Utility implements MetricsInterface { context: Context, callback: Callback ): Promise { - metricsRef.functionName = context.functionName; - if (captureColdStartMetric) metricsRef.captureColdStartMetric(); + if (captureColdStartMetric) { + metricsRef.captureColdStartMetric(context.functionName); + } let result: unknown; try { @@ -749,28 +753,14 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Set the function name to be added to each metric as a dimension. - * - * When using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the function - * name is automatically inferred from the Lambda context. - * - * @example - * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; - * - * const metrics = new Metrics({ - * namespace: 'serverlessAirline', - * serviceName: 'orders' - * }); - * - * metrics.setFunctionName('my-function-name'); - * ``` - * - * @param name - The function name + * @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 */ - public setFunctionName(name: string): void { + /* v8 ignore start */ public setFunctionName(name: string): void { this.functionName = name; - } + } /* v8 ignore end */ /** * Set the flag to throw an error if no metrics are emitted. @@ -917,6 +907,19 @@ class Metrics extends Utility implements MetricsInterface { this.envVarsService = new EnvironmentVariablesService(); } + /** + * 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 + */ + protected setFunctionNameForColdStartMetric(functionName?: string): void { + const value = + functionName?.trim() ?? this.getEnvVarsService().getFunctionName().trim(); + if (value && value.length > 0) { + this.functionName = value; + } + } + /** * Set the namespace to be used. * @@ -951,6 +954,7 @@ class Metrics extends Utility implements MetricsInterface { serviceName, singleMetric, defaultDimensions, + functionName, } = options; this.setEnvVarsService(); @@ -960,6 +964,7 @@ class Metrics extends Utility implements MetricsInterface { this.setNamespace(namespace); this.setService(serviceName); this.setDefaultDimensions(defaultDimensions); + this.setFunctionNameForColdStartMetric(functionName); this.isSingleMetric = singleMetric || false; return this; diff --git a/packages/metrics/src/config/EnvironmentVariablesService.ts b/packages/metrics/src/config/EnvironmentVariablesService.ts index a611308649..6fb6c6719c 100644 --- a/packages/metrics/src/config/EnvironmentVariablesService.ts +++ b/packages/metrics/src/config/EnvironmentVariablesService.ts @@ -11,8 +11,8 @@ class EnvironmentVariablesService extends CommonEnvironmentVariablesService implements ConfigServiceInterface { - private namespaceVariable = 'POWERTOOLS_METRICS_NAMESPACE'; - + private readonly namespaceVariable = 'POWERTOOLS_METRICS_NAMESPACE'; + private readonly functionNameVariable = 'POWERTOOLS_METRICS_FUNCTION_NAME'; private readonly disabledVariable = 'POWERTOOLS_METRICS_DISABLED'; /** @@ -22,6 +22,13 @@ class EnvironmentVariablesService return this.get(this.namespaceVariable); } + /** + * Get the value of the `POWERTOOLS_METRICS_FUNCTION_NAME` environment variable. + */ + public getFunctionName(): string { + return this.get(this.functionNameVariable); + } + /** * Get the value of the `POWERTOOLS_METRICS_DISABLED` or `POWERTOOLS_DEV` environment variables. * diff --git a/packages/metrics/src/middleware/middy.ts b/packages/metrics/src/middleware/middy.ts index e02a39d79a..d06a0977e4 100644 --- a/packages/metrics/src/middleware/middy.ts +++ b/packages/metrics/src/middleware/middy.ts @@ -62,7 +62,6 @@ const logMetrics = ( const logMetricsBefore = async (request: MiddyLikeRequest): Promise => { for (const metrics of metricsInstances) { - metrics.setFunctionName(request.context.functionName); const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (throwOnEmptyMetrics) { @@ -72,7 +71,7 @@ const logMetrics = ( metrics.setDefaultDimensions(defaultDimensions); } if (captureColdStartMetric) { - metrics.captureColdStartMetric(); + metrics.captureColdStartMetric(request.context.functionName); } } diff --git a/packages/metrics/src/types/ConfigServiceInterface.ts b/packages/metrics/src/types/ConfigServiceInterface.ts index 81caa2d53b..68a86e1571 100644 --- a/packages/metrics/src/types/ConfigServiceInterface.ts +++ b/packages/metrics/src/types/ConfigServiceInterface.ts @@ -12,6 +12,10 @@ interface ConfigServiceInterface extends ConfigServiceBaseInterface { * Get the value of the `POWERTOOLS_METRICS_NAMESPACE` environment variable. */ getNamespace(): string; + /** + * Get the value of the `POWERTOOLS_METRICS_FUNCTION_NAME` environment variable. + */ + getFunctionName(): string; } export type { ConfigServiceInterface }; diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index da71711501..ccd914351e 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -60,6 +60,19 @@ type MetricsOptions = { * @see {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} */ defaultDimensions?: Dimensions; + /** + * Function name to use as dimension for the `ColdStart` metric. + * + * When not provided, the function name is inferred either via: + * - `POWERTOOLS_FUNCTION_NAME` environment variable + * - AWS Lambda function context, **only** when using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator or the Middy.js middleware + * - `functionName` parameter in the {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`} method + * + * If none of the above are available, the `ColdStart` metric will not include a function name dimension. + * + * @see {@link MetricsInterface.setFunctionName | `setFunctionName()`} + */ + functionName?: string; /** * Logger object to be used for emitting debug, warning, and error messages. * @@ -272,8 +285,10 @@ interface MetricsInterface { * 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(): void; + captureColdStartMetric(functionName?: string): void; /** * Clear all previously set default dimensions. * @@ -445,24 +460,10 @@ interface MetricsInterface { */ setDefaultDimensions(dimensions: Dimensions | undefined): void; /** - * Set the function name to be added to each metric as a dimension. - * - * When using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the function - * name is automatically inferred from the Lambda context. - * - * @example - * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; - * - * const metrics = new Metrics({ - * namespace: 'serverlessAirline', - * serviceName: 'orders' - * }); - * - * metrics.setFunctionName('my-function-name'); - * ``` - * - * @param name - The function name + * @deprecated Override the function name for `ColdStart` metrics inferred from the context either via: + * - `functionName` constructor parameter + * - `POWERTOOLS_FUNCTION_NAME` environment variable + * - {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`} method */ setFunctionName(name: string): void; /** diff --git a/packages/metrics/tests/unit/coldStartMetric.test.ts b/packages/metrics/tests/unit/coldStartMetric.test.ts index 9a9cba37c9..e63ee9eb72 100644 --- a/packages/metrics/tests/unit/coldStartMetric.test.ts +++ b/packages/metrics/tests/unit/coldStartMetric.test.ts @@ -65,27 +65,62 @@ describe('ColdStart metric', () => { ); }); - it('includes the function name in the cold start metric', () => { + it('does not override the function name from constructor in the cold start metric', () => { // Prepare const functionName = 'my-function'; const metrics = new Metrics({ namespace: DEFAULT_NAMESPACE, + functionName: 'another-function', }); - metrics.setFunctionName(functionName); // Act - metrics.captureColdStartMetric(); + metrics.captureColdStartMetric(functionName); // Assess expect(console.log).toHaveEmittedEMFWith( expect.objectContaining({ service: 'hello-world', [COLD_START_METRIC]: 1, - function_name: 'my-function', + function_name: 'another-function', }) ); }); + it.each([ + { + case: 'empty string', + functionName: '', + }, + { + case: 'undefined', + functionName: undefined, + }, + ])( + 'does not include the function name if not set or invalid ($case)', + ({ functionName }) => { + // Prepare + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + // Act + metrics.captureColdStartMetric(functionName); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + }) + ); + expect(console.log).toHaveEmittedEMFWith( + expect.not.objectContaining({ + function_name: 'my-function', + }) + ); + } + ); + it('emits the metric only once', () => { // Prepare const metrics = new Metrics({ diff --git a/packages/metrics/tests/unit/initializeMetrics.test.ts b/packages/metrics/tests/unit/initializeMetrics.test.ts index 663328a294..a6b4a62a9d 100644 --- a/packages/metrics/tests/unit/initializeMetrics.test.ts +++ b/packages/metrics/tests/unit/initializeMetrics.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_NAMESPACE } from '../../src/constants.js'; +import { COLD_START_METRIC, DEFAULT_NAMESPACE } from '../../src/constants.js'; import { MetricUnit, Metrics } from '../../src/index.js'; import type { ConfigServiceInterface } from '../../src/types/index.js'; @@ -113,6 +113,123 @@ describe('Initialize Metrics', () => { ); }); + it('prioritizes the function name provided in the constructor', () => { + // Prepare + process.env.POWERTOOLS_METRICS_FUNCTION_NAME = 'another-function'; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + functionName: 'my-function-name', + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + function_name: 'my-function-name', + }) + ); + }); + + it('uses the function name provided in the environment variables', () => { + // Prepare + process.env.POWERTOOLS_METRICS_FUNCTION_NAME = 'another-function'; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + function_name: 'another-function', + }) + ); + }); + + it.each([ + { + case: 'an empty string', + functionName: '', + }, + { + case: 'undefined', + functionName: undefined, + }, + ])( + 'does not set the function name from env when is $case', + ({ functionName }) => { + // Prepare + process.env.POWERTOOLS_METRICS_FUNCTION_NAME = functionName; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + }) + ); + expect(console.log).toHaveEmittedEMFWith( + expect.not.objectContaining({ + function_name: expect.anything(), + }) + ); + } + ); + + it.each([ + { + case: 'an empty string', + functionName: '', + }, + { + case: 'undefined', + functionName: undefined, + }, + ])( + 'does not set the function name from constructor when is $case', + ({ functionName }) => { + // Prepare + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + functionName, + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + }) + ); + expect(console.log).toHaveEmittedEMFWith( + expect.not.objectContaining({ + function_name: expect.anything(), + }) + ); + } + ); + it('uses the custom config service provided', () => { // Prepare const configService = { @@ -128,6 +245,9 @@ describe('Initialize Metrics', () => { isValueTrue(value: string): boolean { return value === 'true'; }, + getFunctionName(): string { + return 'custom-function-name'; + }, }; const metrics = new Metrics({ singleMetric: true, diff --git a/packages/metrics/tests/unit/logMetrics.test.ts b/packages/metrics/tests/unit/logMetrics.test.ts index 90234c1e60..a0e2a33447 100644 --- a/packages/metrics/tests/unit/logMetrics.test.ts +++ b/packages/metrics/tests/unit/logMetrics.test.ts @@ -6,6 +6,11 @@ import { COLD_START_METRIC, DEFAULT_NAMESPACE } from '../../src/constants.js'; import { MetricUnit, Metrics } from '../../src/index.js'; import { logMetrics } from '../../src/middleware/middy.js'; +const contextFunctionName = 'context-function-name'; +const contextWithFunctionName = { + functionName: contextFunctionName, +} as Context; + describe('LogMetrics decorator & Middy.js middleware', () => { const ENVIRONMENT_VARIABLES = process.env; @@ -45,8 +50,8 @@ describe('LogMetrics decorator & Middy.js middleware', () => { const handler = lambda.handler.bind(lambda); // Act - await handler({}, {} as Context); - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); + await handler({}, contextWithFunctionName); // Assess expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(2); @@ -56,6 +61,7 @@ describe('LogMetrics decorator & Middy.js middleware', () => { expect.objectContaining({ [COLD_START_METRIC]: 1, service: 'hello-world', + function_name: contextFunctionName, }) ); expect(console.log).toHaveEmittedNthMetricWith( @@ -82,6 +88,94 @@ describe('LogMetrics decorator & Middy.js middleware', () => { ); }); + it('default function name in the cold start metric to context.functionName when using decorator', async () => { + // Prepare + const metrics = new Metrics({ + singleMetric: false, + namespace: DEFAULT_NAMESPACE, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + class Test { + readonly #metricName: string; + + public constructor(name: string) { + this.#metricName = name; + } + + @metrics.logMetrics({ captureColdStartMetric: true }) + async handler(_event: unknown, _context: Context) { + this.addGreetingMetric(); + } + + addGreetingMetric() { + metrics.addMetric(this.#metricName, MetricUnit.Count, 1); + } + } + const lambda = new Test('greetings'); + const handler = lambda.handler.bind(lambda); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveBeenCalledTimes(2); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: contextFunctionName, + }) + ); + }); + + it('does not override existing function name in the cold start metric when using decorator', async () => { + // Prepare + const functionName = 'function-name'; + const metrics = new Metrics({ + singleMetric: false, + namespace: DEFAULT_NAMESPACE, + functionName, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + class Test { + readonly #metricName: string; + + public constructor(name: string) { + this.#metricName = name; + } + + @metrics.logMetrics({ captureColdStartMetric: true }) + async handler(_event: unknown, _context: Context) { + this.addGreetingMetric(); + } + + addGreetingMetric() { + metrics.addMetric(this.#metricName, MetricUnit.Count, 1); + } + } + const lambda = new Test('greetings'); + const handler = lambda.handler.bind(lambda); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveBeenCalledTimes(2); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: functionName, + }) + ); + }); + it('captures the cold start metric on the first invocation when using the Middy.js middleware', async () => { // Prepare const metrics = new Metrics({ @@ -94,8 +188,8 @@ describe('LogMetrics decorator & Middy.js middleware', () => { }).use(logMetrics(metrics, { captureColdStartMetric: true })); // Act - await handler({}, {} as Context); - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); + await handler({}, contextWithFunctionName); // Assess expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(2); @@ -109,6 +203,60 @@ describe('LogMetrics decorator & Middy.js middleware', () => { ); }); + it('default function name in the cold start metric to context.functionName when using the Middy.js middleware', async () => { + // Prepare + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + const handler = middy(async () => { + metrics.addMetric('greetings', MetricUnit.Count, 1); + }).use(logMetrics(metrics, { captureColdStartMetric: true })); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: contextFunctionName, + }) + ); + }); + + it('does not override existing function name in the cold start metric when using the Middy.js middleware', async () => { + // Prepare + const functionName = 'my-function'; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + functionName, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + const handler = middy(async () => { + metrics.addMetric('greetings', MetricUnit.Count, 1); + }).use(logMetrics(metrics, { captureColdStartMetric: true })); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: functionName, + }) + ); + }); + it('includes default dimensions passed in the decorator', async () => { // Prepare const metrics = new Metrics({ @@ -125,7 +273,7 @@ describe('LogMetrics decorator & Middy.js middleware', () => { const handler = lambda.handler.bind(lambda); // Act - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); // Assess expect(console.log).toHaveBeenCalledTimes(1); @@ -157,7 +305,7 @@ describe('LogMetrics decorator & Middy.js middleware', () => { ); // Act - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); // Assess expect(console.log).toHaveBeenCalledTimes(1); @@ -261,8 +409,8 @@ describe('LogMetrics decorator & Middy.js middleware', () => { .use(myCustomMiddleware()); // Act - await handler({ idx: 0 }, {} as Context); - await handler({ idx: 1 }, {} as Context); + await handler({ idx: 0 }, contextWithFunctionName); + await handler({ idx: 1 }, contextWithFunctionName); // Assess expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(2);