Skip to content

Commit a531b90

Browse files
dreamorosiam29d
andauthored
feat(metrics): add ability to pass custom logger (#3057)
Co-authored-by: Alexander Schueren <[email protected]>
1 parent cd54076 commit a531b90

File tree

9 files changed

+82
-91
lines changed

9 files changed

+82
-91
lines changed

layers/tests/e2e/layerPublisher.class.test.functionCode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { SSMClient } from '@aws-sdk/client-ssm';
1717
const logger = new Logger({
1818
logLevel: 'DEBUG',
1919
});
20-
const metrics = new Metrics();
20+
const metrics = new Metrics({ logger });
2121
const tracer = new Tracer();
2222

2323
// Instantiating these clients and the respective providers/persistence layers

layers/tests/e2e/layerPublisher.test.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,10 @@ describe('Layers E2E tests', () => {
143143
const logs = invocationLogs.getFunctionLogs('WARN');
144144

145145
expect(logs.length).toBe(1);
146-
expect(
147-
invocationLogs.doesAnyFunctionLogsContains(
148-
/Namespace should be defined, default used/,
149-
'WARN'
150-
)
151-
).toBe(true);
152-
/* expect(logEntry.message).toEqual(
146+
const logEntry = TestInvocationLogs.parseFunctionLog(logs[0]);
147+
expect(logEntry.message).toEqual(
153148
'Namespace should be defined, default used'
154-
); */
149+
);
155150
}
156151
);
157152

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// biome-ignore lint/suspicious/noExplicitAny: We intentionally use `any` here to represent any type of data and keep the logger is as flexible as possible.
2+
type Anything = any[];
3+
4+
/**
5+
* Interface for a generic logger object.
6+
*
7+
* This interface is used to define the shape of a logger object that can be passed to a Powertools for AWS utility.
8+
*
9+
* It can be an instance of Logger from Powertools for AWS, or any other logger that implements the same methods.
10+
*/
11+
export interface GenericLogger {
12+
trace?: (...content: Anything) => void;
13+
debug: (...content: Anything) => void;
14+
info: (...content: Anything) => void;
15+
warn: (...content: Anything) => void;
16+
error: (...content: Anything) => void;
17+
}

packages/commons/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type {
55
MiddlewareFn,
66
CleanupFunction,
77
} from './middy.js';
8+
export type { GenericLogger } from './GenericLogger.js';
89
export type { SdkClient, MiddlewareArgsLike } from './awsSdk.js';
910
export type {
1011
JSONPrimitive,

packages/metrics/src/Metrics.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Console } from 'node:console';
22
import { Utility } from '@aws-lambda-powertools/commons';
3-
import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types';
3+
import type {
4+
GenericLogger,
5+
HandlerMethodDecorator,
6+
} from '@aws-lambda-powertools/commons/types';
47
import type { Callback, Context, Handler } from 'aws-lambda';
58
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
69
import {
@@ -159,6 +162,13 @@ class Metrics extends Utility implements MetricsInterface {
159162
*/
160163
private functionName?: string;
161164

165+
/**
166+
* Custom logger object used for emitting debug, warning, and error messages.
167+
*
168+
* Note that this logger is not used for emitting metrics which are emitted to standard output using the `Console` object.
169+
*/
170+
readonly #logger: GenericLogger;
171+
162172
/**
163173
* Flag indicating if this is a single metric instance
164174
* @default false
@@ -193,6 +203,7 @@ class Metrics extends Utility implements MetricsInterface {
193203

194204
this.dimensions = {};
195205
this.setOptions(options);
206+
this.#logger = options.logger || this.console;
196207
}
197208

198209
/**
@@ -439,6 +450,13 @@ class Metrics extends Utility implements MetricsInterface {
439450
this.storedMetrics = {};
440451
}
441452

453+
/**
454+
* Check if there are stored metrics in the buffer.
455+
*/
456+
public hasStoredMetrics(): boolean {
457+
return Object.keys(this.storedMetrics).length > 0;
458+
}
459+
442460
/**
443461
* A class method decorator to automatically log metrics after the method returns or throws an error.
444462
*
@@ -539,9 +557,9 @@ class Metrics extends Utility implements MetricsInterface {
539557
* ```
540558
*/
541559
public publishStoredMetrics(): void {
542-
const hasMetrics = Object.keys(this.storedMetrics).length > 0;
560+
const hasMetrics = this.hasStoredMetrics();
543561
if (!this.shouldThrowOnEmptyMetrics && !hasMetrics) {
544-
console.warn(
562+
this.#logger.warn(
545563
'No application metrics to publish. The cold-start metric may be published if enabled. ' +
546564
'If application metrics should never be empty, consider using `throwOnEmptyMetrics`'
547565
);
@@ -584,7 +602,7 @@ class Metrics extends Utility implements MetricsInterface {
584602
}
585603

586604
if (!this.namespace)
587-
console.warn('Namespace should be defined, default used');
605+
this.#logger.warn('Namespace should be defined, default used');
588606

589607
// We reduce the stored metrics to a single object with the metric
590608
// name as the key and the value as the value.
@@ -731,6 +749,7 @@ class Metrics extends Utility implements MetricsInterface {
731749
serviceName: this.dimensions.service,
732750
defaultDimensions: this.defaultDimensions,
733751
singleMetric: true,
752+
logger: this.#logger,
734753
});
735754
}
736755

packages/metrics/src/types/Metrics.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types';
1+
import type {
2+
GenericLogger,
3+
HandlerMethodDecorator,
4+
} from '@aws-lambda-powertools/commons/types';
25
import type {
36
MetricResolution as MetricResolutions,
47
MetricUnit as MetricUnits,
@@ -57,6 +60,15 @@ type MetricsOptions = {
5760
* @see {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`}
5861
*/
5962
defaultDimensions?: Dimensions;
63+
/**
64+
* Logger object to be used for emitting debug, warning, and error messages.
65+
*
66+
* If not provided, debug messages will be suppressed, and warning and error messages will be sent to stdout.
67+
*
68+
* Note that EMF metrics are always sent directly to stdout, regardless of the logger
69+
* to avoid compatibility issues with custom loggers.
70+
*/
71+
logger?: GenericLogger;
6072
};
6173

6274
/**

packages/metrics/tests/unit/Metrics.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jest.mock('node:console', () => ({
2727
...jest.requireActual('node:console'),
2828
Console: jest.fn().mockImplementation(() => ({
2929
log: jest.fn(),
30+
warn: jest.fn(),
31+
debug: jest.fn(),
3032
})),
3133
}));
3234
jest.spyOn(console, 'warn').mockImplementation(() => ({}));
@@ -1254,9 +1256,17 @@ describe('Class: Metrics', () => {
12541256
describe('Methods: publishStoredMetrics', () => {
12551257
test('it should log warning if no metrics are added & throwOnEmptyMetrics is false', () => {
12561258
// Prepare
1257-
const metrics: Metrics = new Metrics({ namespace: TEST_NAMESPACE });
1258-
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
1259-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
1259+
const customLogger = {
1260+
warn: jest.fn(),
1261+
debug: jest.fn(),
1262+
error: jest.fn(),
1263+
info: jest.fn(),
1264+
};
1265+
const metrics: Metrics = new Metrics({
1266+
namespace: TEST_NAMESPACE,
1267+
logger: customLogger,
1268+
});
1269+
const consoleWarnSpy = jest.spyOn(customLogger, 'warn');
12601270

12611271
// Act
12621272
metrics.publishStoredMetrics();
@@ -1266,7 +1276,6 @@ describe('Class: Metrics', () => {
12661276
expect(consoleWarnSpy).toHaveBeenCalledWith(
12671277
'No application metrics to publish. The cold-start metric may be published if enabled. If application metrics should never be empty, consider using `throwOnEmptyMetrics`'
12681278
);
1269-
expect(consoleLogSpy).not.toHaveBeenCalled();
12701279
});
12711280

12721281
test('it should call serializeMetrics && log the stringified return value of serializeMetrics', () => {
@@ -1355,8 +1364,14 @@ describe('Class: Metrics', () => {
13551364
test('it should print warning, if no namespace provided in constructor or environment variable', () => {
13561365
// Prepare
13571366
process.env.POWERTOOLS_METRICS_NAMESPACE = '';
1358-
const metrics: Metrics = new Metrics();
1359-
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
1367+
const customLogger = {
1368+
warn: jest.fn(),
1369+
debug: jest.fn(),
1370+
error: jest.fn(),
1371+
info: jest.fn(),
1372+
};
1373+
const metrics: Metrics = new Metrics({ logger: customLogger });
1374+
const consoleWarnSpy = jest.spyOn(customLogger, 'warn');
13601375

13611376
// Act
13621377
metrics.serializeMetrics();

packages/metrics/tests/unit/middleware/middy.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ jest.mock('node:console', () => ({
1414
...jest.requireActual('node:console'),
1515
Console: jest.fn().mockImplementation(() => ({
1616
log: jest.fn(),
17+
warn: jest.fn(),
18+
debug: jest.fn(),
1719
})),
1820
}));
1921
jest.spyOn(console, 'warn').mockImplementation(() => ({}));
@@ -68,6 +70,7 @@ describe('Middy middleware', () => {
6870
const metrics = new Metrics({
6971
namespace: 'serverlessAirline',
7072
serviceName: 'orders',
73+
logger: console,
7174
});
7275
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
7376
const handler = middy(async (): Promise<void> => undefined).use(

packages/metrics/tests/unit/repro.test.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)