@leanstacks/lambda-utils 0.1.0-alpha.2 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug.md +47 -0
- package/.github/ISSUE_TEMPLATE/story.md +25 -0
- package/.github/ISSUE_TEMPLATE/task.md +15 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +39 -0
- package/README.md +178 -1
- package/docs/LOGGING.md +333 -0
- package/docs/README.md +14 -35
- package/jest.config.ts +4 -4
- package/package.json +8 -9
- package/src/index.ts +1 -1
- package/src/logging/logger.test.ts +400 -0
- package/src/logging/logger.ts +62 -73
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -90
- package/dist/logging/logger.d.ts +0 -57
package/docs/README.md
CHANGED
|
@@ -2,47 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Welcome to the Lambda Utilities documentation. This library provides a comprehensive set of utilities and helper functions to streamline the development of AWS Lambda functions using TypeScript.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- [Logging](./LOGGING.md)
|
|
9
|
-
- [API Gateway Responses](./API_GATEWAY_RESPONSES.md)
|
|
10
|
-
- [Configuration](./CONFIGURATION.md)
|
|
11
|
-
- [Clients](./CLIENTS.md)
|
|
7
|
+
Lambda Utilities is a collection of pre-configured tools and helpers designed to reduce boilerplate code when developing AWS Lambda functions. It provides utilities for logging, API responses, configuration validation, and AWS SDK client management—all with full TypeScript support.
|
|
12
8
|
|
|
13
|
-
##
|
|
9
|
+
## Documentation
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Use a utility in your Lambda function:
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { getLogger } from '@leanstacks/lambda-utils';
|
|
25
|
-
import { success } from '@leanstacks/lambda-utils';
|
|
26
|
-
|
|
27
|
-
const logger = getLogger();
|
|
28
|
-
|
|
29
|
-
export const handler = async (event: any) => {
|
|
30
|
-
logger.info({ message: 'Processing event', event });
|
|
31
|
-
|
|
32
|
-
// Your handler logic here
|
|
33
|
-
|
|
34
|
-
return success({ message: 'Success' });
|
|
35
|
-
};
|
|
36
|
-
```
|
|
11
|
+
- **[Logging Guide](./LOGGING.md)** – Implement structured logging in your Lambda functions with Pino and automatic AWS context enrichment
|
|
12
|
+
- **[API Gateway Responses](./API_GATEWAY_RESPONSES.md)** – Format Lambda responses for API Gateway with standard HTTP status codes and headers
|
|
13
|
+
- **[Configuration](./CONFIGURATION.md)** – Validate environment variables and configuration with Zod type safety
|
|
14
|
+
- **[AWS Clients](./CLIENTS.md)** – Pre-configured AWS SDK v3 clients optimized for Lambda
|
|
15
|
+
- **[Getting Started](./GETTING_STARTED.md)** – Quick setup and installation instructions
|
|
37
16
|
|
|
38
17
|
## Features
|
|
39
18
|
|
|
40
|
-
- **
|
|
41
|
-
- **API
|
|
42
|
-
- **Configuration
|
|
43
|
-
- **AWS Clients
|
|
44
|
-
- **Type Safe
|
|
19
|
+
- 📝 **Structured Logging** – Pino logger pre-configured for Lambda with automatic request context
|
|
20
|
+
- 📤 **API Response Helpers** – Standard response formatting for API Gateway integration
|
|
21
|
+
- ⚙️ **Configuration Validation** – Environment variable validation with Zod schema support
|
|
22
|
+
- 🔌 **AWS Clients** – Pre-configured AWS SDK v3 clients for common services
|
|
23
|
+
- 🔒 **Type Safe** – Full TypeScript support with comprehensive type definitions
|
|
45
24
|
|
|
46
25
|
## Support
|
|
47
26
|
|
|
48
|
-
For issues or questions,
|
|
27
|
+
For issues or questions, visit the [GitHub repository](https://github.com/leanstacks/lambda-utils).
|
package/jest.config.ts
CHANGED
|
@@ -3,13 +3,13 @@ import type { Config } from 'jest';
|
|
|
3
3
|
const config: Config = {
|
|
4
4
|
preset: 'ts-jest',
|
|
5
5
|
testEnvironment: 'node',
|
|
6
|
-
|
|
7
|
-
testMatch: ['**/*.test.ts'],
|
|
6
|
+
testMatch: ['<rootDir>/src/**/*.test.ts'],
|
|
8
7
|
moduleNameMapper: {
|
|
9
8
|
'^@/(.*)$': '<rootDir>/$1',
|
|
10
9
|
},
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
coverageDirectory: 'coverage',
|
|
11
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/index.ts'],
|
|
12
|
+
coverageReporters: ['json', 'json-summary', 'lcov', 'text', 'clover'],
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export default config;
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leanstacks/lambda-utils",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "A collection of utilities and helper functions designed to streamline the development of AWS Lambda functions using TypeScript.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "rollup -c",
|
|
9
9
|
"build:watch": "rollup -c -w",
|
|
10
|
-
"clean": "rimraf dist",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"test:coverage": "jest --coverage",
|
|
10
|
+
"clean": "rimraf coverage dist",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
12
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
14
13
|
"lint": "eslint src",
|
|
15
14
|
"lint:fix": "eslint src --fix",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"test:coverage": "jest --coverage"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"lambda",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"pino": "10.1.0",
|
|
47
|
-
"pino-lambda": "4.4.1"
|
|
48
|
-
"zod": "4.2.1"
|
|
47
|
+
"pino-lambda": "4.4.1"
|
|
49
48
|
}
|
|
50
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { Logger, LoggerConfig, withRequestTracking } from './logging/logger';
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
import { CloudwatchLogFormatter, pinoLambdaDestination, StructuredLogFormatter } from 'pino-lambda';
|
|
4
|
+
import { Logger, withRequestTracking } from './logger';
|
|
5
|
+
|
|
6
|
+
// Mock pino-lambda module
|
|
7
|
+
jest.mock('pino-lambda');
|
|
8
|
+
|
|
9
|
+
// Mock pino module
|
|
10
|
+
jest.mock('pino');
|
|
11
|
+
|
|
12
|
+
describe('Logger', () => {
|
|
13
|
+
// Setup and cleanup
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('withRequestTracking', () => {
|
|
23
|
+
it('should be exported from logger module', () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
// withRequestTracking is exported from logger.ts and is the result of calling lambdaRequestTracker()
|
|
26
|
+
// from pino-lambda. Jest mocks mean it will be the mocked value.
|
|
27
|
+
|
|
28
|
+
// Act & Assert
|
|
29
|
+
// We just verify that it was exported (defined by the import statement at the top)
|
|
30
|
+
// The actual functionality of lambdaRequestTracker is tested in pino-lambda
|
|
31
|
+
expect(typeof withRequestTracking === 'function' || withRequestTracking === undefined).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('constructor', () => {
|
|
36
|
+
it('should create Logger with default configuration', () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const mockLogger = { info: jest.fn() };
|
|
39
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
40
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
const logger = new Logger();
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(logger).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should create Logger with custom enabled configuration', () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const mockLogger = { info: jest.fn() };
|
|
52
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
53
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
54
|
+
|
|
55
|
+
// Act
|
|
56
|
+
const logger = new Logger({ enabled: false });
|
|
57
|
+
const _instance = logger.instance;
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(pino).toHaveBeenCalledWith(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
enabled: false,
|
|
63
|
+
}),
|
|
64
|
+
expect.anything(),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should create Logger with custom log level configuration', () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const mockLogger = { info: jest.fn() };
|
|
71
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
72
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
const logger = new Logger({ level: 'debug' });
|
|
76
|
+
const _instance = logger.instance;
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(pino).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
level: 'debug',
|
|
82
|
+
}),
|
|
83
|
+
expect.anything(),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should create Logger with custom format configuration (json)', () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const mockLogger = { info: jest.fn() };
|
|
90
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
91
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
const logger = new Logger({ format: 'json' });
|
|
95
|
+
const _instance = logger.instance;
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
expect(StructuredLogFormatter).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should create Logger with custom format configuration (text)', () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const mockLogger = { info: jest.fn() };
|
|
104
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
105
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
const logger = new Logger({ format: 'text' });
|
|
109
|
+
const _instance = logger.instance;
|
|
110
|
+
|
|
111
|
+
// Assert
|
|
112
|
+
expect(CloudwatchLogFormatter).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should merge provided config with defaults', () => {
|
|
116
|
+
// Arrange
|
|
117
|
+
const mockLogger = { info: jest.fn() };
|
|
118
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
119
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
120
|
+
|
|
121
|
+
// Act
|
|
122
|
+
const logger = new Logger({ level: 'error' });
|
|
123
|
+
const _instance = logger.instance;
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
expect(pino).toHaveBeenCalledWith(
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
enabled: true,
|
|
129
|
+
level: 'error',
|
|
130
|
+
}),
|
|
131
|
+
expect.anything(),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('instance getter', () => {
|
|
137
|
+
it('should return a Pino logger instance', () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
const mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() };
|
|
140
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
141
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
142
|
+
const logger = new Logger();
|
|
143
|
+
|
|
144
|
+
// Act
|
|
145
|
+
const instance = logger.instance;
|
|
146
|
+
|
|
147
|
+
// Assert
|
|
148
|
+
expect(instance).toBe(mockLogger);
|
|
149
|
+
expect(instance).toHaveProperty('info');
|
|
150
|
+
expect(instance).toHaveProperty('warn');
|
|
151
|
+
expect(instance).toHaveProperty('error');
|
|
152
|
+
expect(instance).toHaveProperty('debug');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should create logger instance only once (lazy initialization)', () => {
|
|
156
|
+
// Arrange
|
|
157
|
+
const mockLogger = { info: jest.fn() };
|
|
158
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
159
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
160
|
+
const logger = new Logger();
|
|
161
|
+
|
|
162
|
+
// Act
|
|
163
|
+
const instance1 = logger.instance;
|
|
164
|
+
const instance2 = logger.instance;
|
|
165
|
+
|
|
166
|
+
// Assert
|
|
167
|
+
expect(instance1).toBe(instance2);
|
|
168
|
+
expect(pino).toHaveBeenCalledTimes(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should configure pino with enabled flag', () => {
|
|
172
|
+
// Arrange
|
|
173
|
+
const mockLogger = { info: jest.fn() };
|
|
174
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
175
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
const logger = new Logger({ enabled: false });
|
|
179
|
+
const _instance = logger.instance;
|
|
180
|
+
|
|
181
|
+
// Assert
|
|
182
|
+
expect(pino).toHaveBeenCalledWith(
|
|
183
|
+
expect.objectContaining({
|
|
184
|
+
enabled: false,
|
|
185
|
+
}),
|
|
186
|
+
expect.anything(),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should configure pino with log level', () => {
|
|
191
|
+
// Arrange
|
|
192
|
+
const mockLogger = { info: jest.fn() };
|
|
193
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
194
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
195
|
+
|
|
196
|
+
// Act
|
|
197
|
+
const logger = new Logger({ level: 'warn' });
|
|
198
|
+
const _instance = logger.instance;
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
201
|
+
expect(pino).toHaveBeenCalledWith(
|
|
202
|
+
expect.objectContaining({
|
|
203
|
+
level: 'warn',
|
|
204
|
+
}),
|
|
205
|
+
expect.anything(),
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should call pinoLambdaDestination with selected formatter', () => {
|
|
210
|
+
// Arrange
|
|
211
|
+
const mockLogger = { info: jest.fn() };
|
|
212
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
213
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
214
|
+
|
|
215
|
+
// Act
|
|
216
|
+
const logger = new Logger({ format: 'json' });
|
|
217
|
+
const _instance = logger.instance;
|
|
218
|
+
|
|
219
|
+
// Assert
|
|
220
|
+
expect(pinoLambdaDestination).toHaveBeenCalledWith(
|
|
221
|
+
expect.objectContaining({
|
|
222
|
+
formatter: expect.any(StructuredLogFormatter),
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should use StructuredLogFormatter when format is json', () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
const mockLogger = { info: jest.fn() };
|
|
230
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
231
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
232
|
+
|
|
233
|
+
// Act
|
|
234
|
+
const logger = new Logger({ format: 'json' });
|
|
235
|
+
const _instance = logger.instance;
|
|
236
|
+
|
|
237
|
+
// Assert
|
|
238
|
+
expect(StructuredLogFormatter).toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should use CloudwatchLogFormatter when format is text', () => {
|
|
242
|
+
// Arrange
|
|
243
|
+
const mockLogger = { info: jest.fn() };
|
|
244
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
245
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
246
|
+
|
|
247
|
+
// Act
|
|
248
|
+
const logger = new Logger({ format: 'text' });
|
|
249
|
+
const _instance = logger.instance;
|
|
250
|
+
|
|
251
|
+
// Assert
|
|
252
|
+
expect(CloudwatchLogFormatter).toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Logger configurations', () => {
|
|
257
|
+
it('should support all log levels', () => {
|
|
258
|
+
// Arrange
|
|
259
|
+
const mockLogger = { info: jest.fn() };
|
|
260
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
261
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
262
|
+
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['debug', 'info', 'warn', 'error'];
|
|
263
|
+
|
|
264
|
+
// Act & Assert
|
|
265
|
+
levels.forEach((level) => {
|
|
266
|
+
jest.clearAllMocks();
|
|
267
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
268
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
269
|
+
|
|
270
|
+
const logger = new Logger({ level });
|
|
271
|
+
const _instance = logger.instance;
|
|
272
|
+
|
|
273
|
+
expect(pino).toHaveBeenCalledWith(
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
level,
|
|
276
|
+
}),
|
|
277
|
+
expect.anything(),
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should support both json and text formats', () => {
|
|
283
|
+
// Arrange
|
|
284
|
+
const mockLogger = { info: jest.fn() };
|
|
285
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
286
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
287
|
+
|
|
288
|
+
// Act
|
|
289
|
+
const jsonLogger = new Logger({ format: 'json' });
|
|
290
|
+
const _jsonInstance = jsonLogger.instance;
|
|
291
|
+
const structuredFormatterCallCount = (StructuredLogFormatter as jest.Mock).mock.calls.length;
|
|
292
|
+
|
|
293
|
+
jest.clearAllMocks();
|
|
294
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
295
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
296
|
+
|
|
297
|
+
const textLogger = new Logger({ format: 'text' });
|
|
298
|
+
const _textInstance = textLogger.instance;
|
|
299
|
+
|
|
300
|
+
// Assert
|
|
301
|
+
expect(structuredFormatterCallCount).toBeGreaterThan(0);
|
|
302
|
+
expect(CloudwatchLogFormatter).toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should support enabled and disabled logging', () => {
|
|
306
|
+
// Arrange
|
|
307
|
+
const mockLogger = { info: jest.fn() };
|
|
308
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
309
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
310
|
+
|
|
311
|
+
// Act
|
|
312
|
+
const enabledLogger = new Logger({ enabled: true });
|
|
313
|
+
const _enabledInstance = enabledLogger.instance;
|
|
314
|
+
const firstCallArgs = jest.mocked(pino).mock.calls[0];
|
|
315
|
+
|
|
316
|
+
jest.clearAllMocks();
|
|
317
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
318
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
319
|
+
|
|
320
|
+
const disabledLogger = new Logger({ enabled: false });
|
|
321
|
+
const _disabledInstance = disabledLogger.instance;
|
|
322
|
+
const secondCallArgs = jest.mocked(pino).mock.calls[0];
|
|
323
|
+
|
|
324
|
+
// Assert
|
|
325
|
+
expect(firstCallArgs[0]).toEqual(expect.objectContaining({ enabled: true }));
|
|
326
|
+
expect(secondCallArgs[0]).toEqual(expect.objectContaining({ enabled: false }));
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('integration scenarios', () => {
|
|
331
|
+
it('should create multiple logger instances with different configurations', () => {
|
|
332
|
+
// Arrange
|
|
333
|
+
const mockLogger1 = { info: jest.fn(), level: 'debug' };
|
|
334
|
+
const mockLogger2 = { info: jest.fn(), level: 'error' };
|
|
335
|
+
jest
|
|
336
|
+
.mocked(pino)
|
|
337
|
+
.mockReturnValueOnce(mockLogger1 as unknown as any)
|
|
338
|
+
.mockReturnValueOnce(mockLogger2 as unknown as any);
|
|
339
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
340
|
+
|
|
341
|
+
// Act
|
|
342
|
+
const debugLogger = new Logger({ level: 'debug', format: 'json' });
|
|
343
|
+
const errorLogger = new Logger({ level: 'error', format: 'text' });
|
|
344
|
+
|
|
345
|
+
const instance1 = debugLogger.instance;
|
|
346
|
+
const instance2 = errorLogger.instance;
|
|
347
|
+
|
|
348
|
+
// Assert
|
|
349
|
+
expect(instance1).toBe(mockLogger1);
|
|
350
|
+
expect(instance2).toBe(mockLogger2);
|
|
351
|
+
expect(pino).toHaveBeenCalledTimes(2);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should handle partial configuration overrides', () => {
|
|
355
|
+
// Arrange
|
|
356
|
+
const mockLogger = { info: jest.fn() };
|
|
357
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
358
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
359
|
+
|
|
360
|
+
// Act
|
|
361
|
+
const logger = new Logger({ level: 'warn' });
|
|
362
|
+
const _instance = logger.instance;
|
|
363
|
+
|
|
364
|
+
// Assert - should have custom level but default enabled and format
|
|
365
|
+
expect(pino).toHaveBeenCalledWith(
|
|
366
|
+
expect.objectContaining({
|
|
367
|
+
enabled: true,
|
|
368
|
+
level: 'warn',
|
|
369
|
+
}),
|
|
370
|
+
expect.anything(),
|
|
371
|
+
);
|
|
372
|
+
expect(StructuredLogFormatter).toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should handle full configuration override', () => {
|
|
376
|
+
// Arrange
|
|
377
|
+
const mockLogger = { info: jest.fn() };
|
|
378
|
+
jest.mocked(pino).mockReturnValue(mockLogger as unknown as any);
|
|
379
|
+
(pinoLambdaDestination as jest.Mock).mockReturnValue({});
|
|
380
|
+
|
|
381
|
+
// Act
|
|
382
|
+
const logger = new Logger({
|
|
383
|
+
enabled: false,
|
|
384
|
+
level: 'error',
|
|
385
|
+
format: 'text',
|
|
386
|
+
});
|
|
387
|
+
const _instance = logger.instance;
|
|
388
|
+
|
|
389
|
+
// Assert
|
|
390
|
+
expect(pino).toHaveBeenCalledWith(
|
|
391
|
+
expect.objectContaining({
|
|
392
|
+
enabled: false,
|
|
393
|
+
level: 'error',
|
|
394
|
+
}),
|
|
395
|
+
expect.anything(),
|
|
396
|
+
);
|
|
397
|
+
expect(CloudwatchLogFormatter).toHaveBeenCalled();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
package/src/logging/logger.ts
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
} from 'pino-lambda';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Logger middleware which adds AWS Lambda attributes to log messages.
|
|
11
|
+
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```typescript
|
|
13
|
-
* import { withRequestTracking } from '@utils
|
|
14
|
+
* import { withRequestTracking } from '@leanstacks/lambda-utils';
|
|
14
15
|
*
|
|
15
16
|
* export const handler = async (event, context) => {
|
|
16
17
|
* withRequestTracking(event, context);
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
export const withRequestTracking = lambdaRequestTracker();
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Configuration options for the
|
|
26
|
+
* Configuration options for the Logger
|
|
26
27
|
*/
|
|
27
28
|
export interface LoggerConfig {
|
|
28
29
|
/** Whether logging is enabled */
|
|
@@ -34,82 +35,70 @@ export interface LoggerConfig {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
*/
|
|
39
|
-
let _loggerConfig: LoggerConfig = {
|
|
40
|
-
enabled: true,
|
|
41
|
-
level: 'info',
|
|
42
|
-
format: 'json',
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Module-level cache for the logger instance
|
|
47
|
-
*/
|
|
48
|
-
let _loggerInstance: pino.Logger | null = null;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Create and return the Pino Lambda logger instance
|
|
52
|
-
* Uses the configuration set by initializeLogger
|
|
53
|
-
* Logger instance is cached after first creation
|
|
54
|
-
*/
|
|
55
|
-
const _createLogger = (): pino.Logger => {
|
|
56
|
-
const formatter = _loggerConfig.format === 'json' ? new StructuredLogFormatter() : new CloudwatchLogFormatter();
|
|
57
|
-
|
|
58
|
-
const lambdaDestination = pinoLambdaDestination({
|
|
59
|
-
formatter,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return pino(
|
|
63
|
-
{
|
|
64
|
-
enabled: _loggerConfig.enabled,
|
|
65
|
-
level: _loggerConfig.level,
|
|
66
|
-
},
|
|
67
|
-
lambdaDestination,
|
|
68
|
-
);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Initialize the logger with configuration
|
|
73
|
-
* Should be called once at Lambda handler entry point
|
|
74
|
-
* Invalidates the cached logger instance so a new one is created with the updated config
|
|
75
|
-
*
|
|
76
|
-
* @param config Logger configuration options
|
|
77
|
-
* @returns void
|
|
38
|
+
* Logger class which provides a Pino logger instance with AWS Lambda attributes.
|
|
78
39
|
*
|
|
79
40
|
* @example
|
|
80
41
|
* ```typescript
|
|
81
|
-
* import {
|
|
42
|
+
* import { Logger } from '@leanstacks/lambda-utils';
|
|
43
|
+
* const logger = new Logger().instance;
|
|
82
44
|
*
|
|
83
|
-
*
|
|
84
|
-
* enabled: true,
|
|
85
|
-
* level: 'debug',
|
|
86
|
-
* format: 'json',
|
|
87
|
-
* });
|
|
45
|
+
* logger.info('Hello, world!');
|
|
88
46
|
* ```
|
|
89
47
|
*/
|
|
90
|
-
export
|
|
91
|
-
_loggerConfig = {
|
|
92
|
-
enabled:
|
|
93
|
-
level:
|
|
94
|
-
format:
|
|
48
|
+
export class Logger {
|
|
49
|
+
private _loggerConfig: LoggerConfig = {
|
|
50
|
+
enabled: true,
|
|
51
|
+
level: 'info',
|
|
52
|
+
format: 'json',
|
|
95
53
|
};
|
|
96
|
-
// Invalidate the cached logger instance so a new one is created with updated config
|
|
97
|
-
_loggerInstance = null;
|
|
98
|
-
};
|
|
99
54
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
export const getLogger = (): pino.Logger => {
|
|
111
|
-
if (_loggerInstance === null) {
|
|
112
|
-
_loggerInstance = _createLogger();
|
|
55
|
+
private _instance: pino.Logger | null = null;
|
|
56
|
+
|
|
57
|
+
constructor(config?: LoggerConfig) {
|
|
58
|
+
if (config) {
|
|
59
|
+
this._loggerConfig = {
|
|
60
|
+
enabled: config.enabled ?? true,
|
|
61
|
+
level: config.level ?? 'info',
|
|
62
|
+
format: config.format ?? 'json',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
113
65
|
}
|
|
114
|
-
|
|
115
|
-
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new, fully configured Pino logger instance.
|
|
69
|
+
*/
|
|
70
|
+
private _createLogger = (): pino.Logger => {
|
|
71
|
+
const formatter =
|
|
72
|
+
this._loggerConfig.format === 'json' ? new StructuredLogFormatter() : new CloudwatchLogFormatter();
|
|
73
|
+
|
|
74
|
+
const lambdaDestination = pinoLambdaDestination({
|
|
75
|
+
formatter,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return pino(
|
|
79
|
+
{
|
|
80
|
+
enabled: this._loggerConfig.enabled,
|
|
81
|
+
level: this._loggerConfig.level,
|
|
82
|
+
},
|
|
83
|
+
lambdaDestination,
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the logger instance.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* import { Logger } from '@leanstacks/lambda-utils';
|
|
93
|
+
* const logger = new Logger().instance;
|
|
94
|
+
*
|
|
95
|
+
* logger.info('Hello, world!');
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
get instance(): pino.Logger {
|
|
99
|
+
if (this._instance === null) {
|
|
100
|
+
this._instance = this._createLogger();
|
|
101
|
+
}
|
|
102
|
+
return this._instance;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/dist/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { getLogger, initializeLogger, LoggerConfig, withRequestTracking } from './logging/logger';
|