Skip to content

feat(metrics): allow setting functionName via constructor parameter and environment variable #3696

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
185e15b
feat(metrics): add ability to pass functionName to middy and decorator
steven10172 Mar 4, 2025
ff79706
docs(metrics): add setting function name section
steven10172 Mar 4, 2025
27875ee
docs(metrics): updated highlighted line for setting function name dec…
steven10172 Mar 4, 2025
68439d6
refactor(metrics): revert setting functionName in middy and decorator…
steven10172 Mar 14, 2025
9efee79
feat(metrics): allow setting functionName via ENV and constructor par…
steven10172 Mar 14, 2025
4de0264
docs(metrics): update docs and mention functionName constructor param…
steven10172 Mar 14, 2025
0a2cec8
Merge branch 'aws-powertools:main' into improv/metrics-no-override-fu…
steven10172 Mar 14, 2025
0616dc5
docs(metrics): add reference to POWERTOOLS_METRICS_FUNCTION_NAME on h…
steven10172 Mar 14, 2025
1c4c25a
refactor(metrics): cleanup code based on sonarqubecloud
steven10172 Mar 14, 2025
2c81643
refactor(metrics): deprecated setFunctionName and expand captureColdS…
steven10172 Mar 19, 2025
1c099b4
docs(metrics): update to become more inline with implementation
steven10172 Mar 19, 2025
61cfd8c
doc updates
steven10172 Mar 19, 2025
55ee400
Merge branch 'aws-powertools:main' into improv/metrics-no-override-fu…
steven10172 Mar 19, 2025
2378cb2
more doc updates
steven10172 Mar 19, 2025
6b008c2
Merge branch 'main' into improv/metrics-no-override-function-name
dreamorosi Mar 20, 2025
f5e5ac0
chore: align with suggested implementation
dreamorosi Mar 20, 2025
21dd0c8
chore: ignore deprecated method from coverage
dreamorosi Mar 20, 2025
29a6280
chore: format table
dreamorosi Mar 20, 2025
d69c2a0
chore: address review comments
dreamorosi Mar 20, 2025
9631937
chore: address sonar
dreamorosi Mar 20, 2025
258becd
Merge branch 'main' into improv/metrics-no-override-function-name
dreamorosi Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,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
Expand Down Expand Up @@ -230,6 +230,36 @@ You can add default dimensions to your metrics by passing them as parameters in

If you'd like to remove them at some point, you can use the `clearDefaultDimensions` method.

### Setting function name

When emitting cold start metrics, we use the `context.functionName` as the `function_name`
dimension. If you want to change the function name you can set the `functionName` by
passing it as a parameter in 3 ways:

* in the [Middy-compatible](https://github.com/middyjs/middy){target=_blank} middleware
* using the `setFunctionName` method
* in the decorator

=== "Middy middleware"

```typescript hl_lines="22"
--8<-- "examples/snippets/metrics/functionNameMiddy.ts"
```

=== "setFunctionName method"

```typescript hl_lines="9"
--8<-- "examples/snippets/metrics/setFunctionName.ts"
```

=== "with logMetrics decorator"

```typescript hl_lines="12"
--8<-- "examples/snippets/metrics/functionNameDecorator.ts"
```

1. Binding your handler method allows your handler to access `this` within the class methods.

### Changing default timestamp

When creating metrics, we use the current timestamp. If you want to change the timestamp of all the metrics you create, utilize the `setTimestamp` function. You can specify a datetime object or an integer representing an epoch timestamp in milliseconds.
Expand Down
21 changes: 21 additions & 0 deletions examples/snippets/metrics/functionNameDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});

export class Lambda implements LambdaInterface {
// Decorate your handler class method
@metrics.logMetrics({
functionName: 'my-function-name',
captureColdStartMetric: true,
})
public async handler(_event: unknown, _context: unknown): Promise<void> {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
}
}

const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
25 changes: 25 additions & 0 deletions examples/snippets/metrics/functionNameMiddy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MetricUnit, Metrics } 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',
});

const lambdaHandler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};

// Wrap the handler with middy
export const handler = middy(lambdaHandler)
// Use the middleware by passing the Metrics instance as a parameter
.use(
logMetrics(metrics, {
functionName: 'my-function-name',
captureColdStartMetric: true,
})
);
19 changes: 19 additions & 0 deletions examples/snippets/metrics/setFunctionName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});

// Setting function name must come before calling `captureColdStartMetric`
metrics.setFunctionName('my-function-name');

// Ensure we emit the cold start
metrics.captureColdStartMetric();

export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
27 changes: 24 additions & 3 deletions packages/metrics/src/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,17 @@ class Metrics extends Utility implements MetricsInterface {
return Object.keys(this.storedMetrics).length > 0;
}

/**
* Check if a function name has been defined.
*
* This is useful when you want to only set a function name if it is not already set.
*
* The method is primarily intended for internal use, but it is exposed for advanced use cases.
*/
public hasFunctionName(): boolean {
return this.functionName != null;
}

/**
* Whether metrics are disabled.
*/
Expand Down Expand Up @@ -518,18 +529,26 @@ class Metrics extends Utility implements MetricsInterface {
* - `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
* - `functionName` - Set the function name used for cold starts
*
* @param options - Options to configure the behavior of the decorator, see {@link ExtraOptions}
*/
public logMetrics(options: ExtraOptions = {}): HandlerMethodDecorator {
const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } =
options;
const {
throwOnEmptyMetrics,
defaultDimensions,
captureColdStartMetric,
functionName,
} = options;
if (throwOnEmptyMetrics) {
this.setThrowOnEmptyMetrics(throwOnEmptyMetrics);
}
if (defaultDimensions !== undefined) {
this.setDefaultDimensions(defaultDimensions);
}
if (functionName !== undefined) {
this.setFunctionName(functionName);
}

return (_target, _propertyKey, descriptor) => {
// biome-ignore lint/style/noNonNullAssertion: The descriptor.value is the method this decorator decorates, it cannot be undefined.
Expand All @@ -543,7 +562,9 @@ class Metrics extends Utility implements MetricsInterface {
context: Context,
callback: Callback
): Promise<unknown> {
metricsRef.functionName = context.functionName;
if (!metricsRef.hasFunctionName()) {
metricsRef.functionName = context.functionName;
}
if (captureColdStartMetric) metricsRef.captureColdStartMetric();

let result: unknown;
Expand Down
12 changes: 9 additions & 3 deletions packages/metrics/src/middleware/middy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,15 @@ const logMetrics = (

const logMetricsBefore = async (request: MiddyLikeRequest): Promise<void> => {
for (const metrics of metricsInstances) {
metrics.setFunctionName(request.context.functionName);
const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } =
options;
const {
throwOnEmptyMetrics,
defaultDimensions,
captureColdStartMetric,
functionName,
} = options;
if (!metrics.hasFunctionName() || functionName) {
metrics.setFunctionName(functionName ?? request.context.functionName);
}
if (throwOnEmptyMetrics) {
metrics.setThrowOnEmptyMetrics(throwOnEmptyMetrics);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/metrics/src/types/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ type ExtraOptions = {
* @see {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`}
*/
captureColdStartMetric?: boolean;
/**
* Set the metric instances function name for `ColdStart` metric.
*
* @default request.context.functionName
* @see {@link MetricsInterface.setFunctionName | `setFunctionName()`}
*/
functionName?: string;
};

/**
Expand Down
139 changes: 139 additions & 0 deletions packages/metrics/tests/unit/logMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,55 @@ describe('LogMetrics decorator & Middy.js middleware', () => {
);
});

it('override the function name for cold start metric when using decorator', async () => {
// Prepare
const decoratorFunctionName = 'decorator-function-name';
const functionName = 'function-name';
const metrics = new Metrics({
singleMetric: false,
namespace: DEFAULT_NAMESPACE,
});
metrics.setFunctionName(functionName);

vi.spyOn(metrics, 'publishStoredMetrics');
class Test {
readonly #metricName: string;

public constructor(name: string) {
this.#metricName = name;
}

@metrics.logMetrics({
captureColdStartMetric: true,
functionName: decoratorFunctionName,
})
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({}, {} as Context);

// 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: decoratorFunctionName,
})
);
});

it('captures the cold start metric on the first invocation when using the Middy.js middleware', async () => {
// Prepare
const metrics = new Metrics({
Expand Down Expand Up @@ -109,6 +158,96 @@ describe('LogMetrics decorator & Middy.js middleware', () => {
);
});

it('sets the function name in the cold start metric when using the Middy.js middleware', async () => {
// Prepare
const contextFunctionName = 'lambda-function-context-name';
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({}, { functionName: contextFunctionName } as Context);

// Assess
expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1);
expect(console.log).toHaveEmittedNthEMFWith(
1,
expect.objectContaining({
[COLD_START_METRIC]: 1,
service: 'hello-world',
function_name: contextFunctionName,
})
);
});

it('override the function name in the cold start metric when using the Middy.js middleware', async () => {
// Prepare
const contextFunctionName = 'lambda-function-context-name';
const functionName = 'my-function';
const metrics = new Metrics({
namespace: DEFAULT_NAMESPACE,
});
metrics.setFunctionName(functionName);

vi.spyOn(metrics, 'publishStoredMetrics');
const overrideFunctionName = 'overwritten-function-name';
const handler = middy(async () => {
metrics.addMetric('greetings', MetricUnit.Count, 1);
}).use(
logMetrics(metrics, {
captureColdStartMetric: true,
functionName: overrideFunctionName,
})
);

// Act
await handler({}, { functionName: contextFunctionName } as Context);

// Assess
expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1);
expect(console.log).toHaveEmittedNthEMFWith(
1,
expect.objectContaining({
[COLD_START_METRIC]: 1,
service: 'hello-world',
function_name: overrideFunctionName,
})
);
});

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,
});
metrics.setFunctionName(functionName);

vi.spyOn(metrics, 'publishStoredMetrics');
const handler = middy(async () => {
metrics.addMetric('greetings', MetricUnit.Count, 1);
}).use(logMetrics(metrics, { captureColdStartMetric: true }));

// Act
await handler({}, {} as Context);

// 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({
Expand Down
Loading