@remit/logger-lambda 0.0.1
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/README.md +45 -0
- package/package.json +35 -0
- package/src/index.ts +2 -0
- package/src/logger.test.ts +196 -0
- package/src/logger.ts +125 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @remit/logger-lambda
|
|
2
|
+
|
|
3
|
+
Shared logging package for Remit Lambda workers. Wraps [Pino](https://getpino.io/) with Lambda-specific configuration and environment-aware formatting.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Structured Logging**: JSON output for CloudWatch ingestion in production.
|
|
8
|
+
- **Pretty Print**: Human-readable output for local development.
|
|
9
|
+
- **Request Context**: Automatically correlates logs with Lambda request IDs.
|
|
10
|
+
- **Zero Configuration**: Sensible defaults based on `NODE_ENV`.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @remit/logger-lambda
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createLogger } from "@remit/logger-lambda";
|
|
22
|
+
import type { SQSEvent, Context } from "aws-lambda";
|
|
23
|
+
|
|
24
|
+
export const handler = async (event: SQSEvent, context: Context) => {
|
|
25
|
+
// Initialize logger with Lambda context
|
|
26
|
+
const log = createLogger(context);
|
|
27
|
+
|
|
28
|
+
log.info({ event }, "Processing SQS event");
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// ... business logic
|
|
32
|
+
log.info("Success");
|
|
33
|
+
} catch (error) {
|
|
34
|
+
log.error({ error }, "Processing failed");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Environment Variables
|
|
41
|
+
|
|
42
|
+
| Variable | Default | Description |
|
|
43
|
+
| ----------- | ------------ | ------------------------------------------------------- |
|
|
44
|
+
| `LOG_LEVEL` | `info` | Pino log level (trace, debug, info, warn, error, fatal) |
|
|
45
|
+
| `NODE_ENV` | `production` | If `development`, enables pretty printing |
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remit/logger-lambda",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test:typecheck": "tsgo --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"build": "npm run test:typecheck"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aws-lambda-powertools/logger": "^2",
|
|
20
|
+
"@aws-lambda-powertools/metrics": "^2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/aws-lambda": "*",
|
|
24
|
+
"vitest": "*"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/remit-mail/remit.git",
|
|
33
|
+
"directory": "packages/logger-lambda"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Context } from "aws-lambda";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mockPublishStoredMetrics = vi.fn();
|
|
5
|
+
const mockAddMetric = vi.fn();
|
|
6
|
+
const mockCaptureColdStartMetric = vi.fn();
|
|
7
|
+
const mockAddContext = vi.fn();
|
|
8
|
+
const mockLogTrace = vi.fn();
|
|
9
|
+
const mockLogDebug = vi.fn();
|
|
10
|
+
const mockLogInfo = vi.fn();
|
|
11
|
+
const mockLogWarn = vi.fn();
|
|
12
|
+
const mockLogError = vi.fn();
|
|
13
|
+
const mockLogCritical = vi.fn();
|
|
14
|
+
const mockAppendPersistentKeys = vi.fn();
|
|
15
|
+
const mockCreateChild = vi.fn();
|
|
16
|
+
|
|
17
|
+
vi.mock("@aws-lambda-powertools/metrics", () => {
|
|
18
|
+
class MockMetrics {
|
|
19
|
+
addMetric = mockAddMetric;
|
|
20
|
+
publishStoredMetrics = mockPublishStoredMetrics;
|
|
21
|
+
captureColdStartMetric = mockCaptureColdStartMetric;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
Metrics: MockMetrics,
|
|
26
|
+
MetricUnit: {
|
|
27
|
+
Count: "Count",
|
|
28
|
+
Milliseconds: "Milliseconds",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
vi.mock("@aws-lambda-powertools/logger", () => {
|
|
34
|
+
class MockLogger {
|
|
35
|
+
addContext = mockAddContext;
|
|
36
|
+
trace = mockLogTrace;
|
|
37
|
+
debug = mockLogDebug;
|
|
38
|
+
info = mockLogInfo;
|
|
39
|
+
warn = mockLogWarn;
|
|
40
|
+
error = mockLogError;
|
|
41
|
+
critical = mockLogCritical;
|
|
42
|
+
appendPersistentKeys = mockAppendPersistentKeys;
|
|
43
|
+
createChild = mockCreateChild;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { Logger: MockLogger };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const makeContext = (): Context =>
|
|
50
|
+
({
|
|
51
|
+
awsRequestId: "test-request-id",
|
|
52
|
+
functionName: "test-function",
|
|
53
|
+
invokedFunctionArn: "arn:aws:lambda:us-east-1:123:function:test",
|
|
54
|
+
memoryLimitInMB: "128",
|
|
55
|
+
logGroupName: "/aws/lambda/test",
|
|
56
|
+
logStreamName: "test-stream",
|
|
57
|
+
getRemainingTimeInMillis: () => 30000,
|
|
58
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
59
|
+
functionVersion: "$LATEST",
|
|
60
|
+
done: vi.fn(),
|
|
61
|
+
fail: vi.fn(),
|
|
62
|
+
succeed: vi.fn(),
|
|
63
|
+
}) as unknown as Context;
|
|
64
|
+
|
|
65
|
+
describe("remit-logger-lambda", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.clearAllMocks();
|
|
68
|
+
mockCreateChild.mockImplementation(() => {
|
|
69
|
+
const child = {
|
|
70
|
+
trace: mockLogTrace,
|
|
71
|
+
debug: mockLogDebug,
|
|
72
|
+
info: mockLogInfo,
|
|
73
|
+
warn: mockLogWarn,
|
|
74
|
+
error: mockLogError,
|
|
75
|
+
critical: mockLogCritical,
|
|
76
|
+
appendPersistentKeys: mockAppendPersistentKeys,
|
|
77
|
+
createChild: mockCreateChild,
|
|
78
|
+
};
|
|
79
|
+
return child;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("exports metrics as a Metrics instance", async () => {
|
|
84
|
+
const { metrics } = await import("./logger.js");
|
|
85
|
+
const { Metrics } = await import("@aws-lambda-powertools/metrics");
|
|
86
|
+
expect(metrics).toBeInstanceOf(Metrics);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("object-first call maps to Powertools message + attributes", async () => {
|
|
90
|
+
const { createLogger } = await import("./logger.js");
|
|
91
|
+
const log = createLogger(makeContext());
|
|
92
|
+
log.error({ error: "boom", messageId: "m1" }, "Failed to parse message");
|
|
93
|
+
expect(mockLogError).toHaveBeenCalledWith("Failed to parse message", {
|
|
94
|
+
error: "boom",
|
|
95
|
+
messageId: "m1",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("object-first call without message uses empty string", async () => {
|
|
100
|
+
const { createLogger } = await import("./logger.js");
|
|
101
|
+
const log = createLogger();
|
|
102
|
+
log.info({ count: 3 });
|
|
103
|
+
expect(mockLogInfo).toHaveBeenCalledWith("", { count: 3 });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("string-first call passes message then attributes", async () => {
|
|
107
|
+
const { createLogger } = await import("./logger.js");
|
|
108
|
+
const log = createLogger();
|
|
109
|
+
log.warn("watch out", { reason: "slow" });
|
|
110
|
+
expect(mockLogWarn).toHaveBeenCalledWith("watch out", { reason: "slow" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("string-first call without attributes passes only the message", async () => {
|
|
114
|
+
const { createLogger } = await import("./logger.js");
|
|
115
|
+
const log = createLogger();
|
|
116
|
+
log.debug("hello");
|
|
117
|
+
expect(mockLogDebug).toHaveBeenCalledWith("hello");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("fatal maps to Powertools critical", async () => {
|
|
121
|
+
const { createLogger } = await import("./logger.js");
|
|
122
|
+
const log = createLogger();
|
|
123
|
+
log.fatal({ fatal: true }, "the end");
|
|
124
|
+
expect(mockLogCritical).toHaveBeenCalledWith("the end", { fatal: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("trace maps to Powertools trace", async () => {
|
|
128
|
+
const { createLogger } = await import("./logger.js");
|
|
129
|
+
const log = createLogger();
|
|
130
|
+
log.trace("trace me");
|
|
131
|
+
expect(mockLogTrace).toHaveBeenCalledWith("trace me");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("child creates a Powertools child and appends bindings", async () => {
|
|
135
|
+
const { createLogger } = await import("./logger.js");
|
|
136
|
+
const log = createLogger();
|
|
137
|
+
const child = log.child({ queue: "imap" });
|
|
138
|
+
expect(mockCreateChild).toHaveBeenCalledOnce();
|
|
139
|
+
expect(mockAppendPersistentKeys).toHaveBeenCalledWith({ queue: "imap" });
|
|
140
|
+
child.info({ done: true }, "child log");
|
|
141
|
+
expect(mockLogInfo).toHaveBeenCalledWith("child log", { done: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("setBindings appends persistent keys", async () => {
|
|
145
|
+
const { createLogger } = await import("./logger.js");
|
|
146
|
+
const log = createLogger();
|
|
147
|
+
log.setBindings({ requestId: "r1" });
|
|
148
|
+
expect(mockAppendPersistentKeys).toHaveBeenCalledWith({ requestId: "r1" });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("withTelemetry calls the handler and returns its result", async () => {
|
|
152
|
+
const { withTelemetry } = await import("./logger.js");
|
|
153
|
+
const handler = vi.fn().mockResolvedValue("hello");
|
|
154
|
+
const wrapped = withTelemetry(handler);
|
|
155
|
+
const result = await wrapped({ key: "value" }, makeContext());
|
|
156
|
+
expect(result).toBe("hello");
|
|
157
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("withTelemetry re-throws handler errors", async () => {
|
|
161
|
+
const { withTelemetry } = await import("./logger.js");
|
|
162
|
+
const boom = new Error("boom");
|
|
163
|
+
const handler = vi.fn().mockRejectedValue(boom);
|
|
164
|
+
const wrapped = withTelemetry(handler);
|
|
165
|
+
await expect(wrapped({}, makeContext())).rejects.toThrow("boom");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("withTelemetry publishes metrics in finally even on error", async () => {
|
|
169
|
+
const { withTelemetry } = await import("./logger.js");
|
|
170
|
+
const handler = vi.fn().mockRejectedValue(new Error("fail"));
|
|
171
|
+
const wrapped = withTelemetry(handler);
|
|
172
|
+
await expect(wrapped({}, makeContext())).rejects.toThrow();
|
|
173
|
+
expect(mockPublishStoredMetrics).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("withTelemetry emits errorCount on handler failure", async () => {
|
|
177
|
+
const { withTelemetry } = await import("./logger.js");
|
|
178
|
+
const handler = vi.fn().mockRejectedValue(new Error("fail"));
|
|
179
|
+
const wrapped = withTelemetry(handler);
|
|
180
|
+
await expect(wrapped({}, makeContext())).rejects.toThrow();
|
|
181
|
+
expect(mockAddMetric).toHaveBeenCalledWith("errorCount", "Count", 1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("withTelemetry emits invocationCount and invocationLatency on success", async () => {
|
|
185
|
+
const { withTelemetry } = await import("./logger.js");
|
|
186
|
+
const handler = vi.fn().mockResolvedValue(42);
|
|
187
|
+
const wrapped = withTelemetry(handler);
|
|
188
|
+
await wrapped({}, makeContext());
|
|
189
|
+
expect(mockAddMetric).toHaveBeenCalledWith("invocationCount", "Count", 1);
|
|
190
|
+
expect(mockAddMetric).toHaveBeenCalledWith(
|
|
191
|
+
"invocationLatency",
|
|
192
|
+
"Milliseconds",
|
|
193
|
+
expect.any(Number),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Logger as PowertoolsLogger } from "@aws-lambda-powertools/logger";
|
|
2
|
+
import { Metrics, MetricUnit } from "@aws-lambda-powertools/metrics";
|
|
3
|
+
import type { Context } from "aws-lambda";
|
|
4
|
+
|
|
5
|
+
type LogBindings = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
type PowertoolsLevel =
|
|
8
|
+
| "trace"
|
|
9
|
+
| "debug"
|
|
10
|
+
| "info"
|
|
11
|
+
| "warn"
|
|
12
|
+
| "error"
|
|
13
|
+
| "critical";
|
|
14
|
+
|
|
15
|
+
const isBindings = (value: unknown): value is LogBindings =>
|
|
16
|
+
typeof value === "object" && value !== null;
|
|
17
|
+
|
|
18
|
+
const emit = (
|
|
19
|
+
target: PowertoolsLogger,
|
|
20
|
+
level: PowertoolsLevel,
|
|
21
|
+
first: LogBindings | string,
|
|
22
|
+
second?: LogBindings | string,
|
|
23
|
+
): void => {
|
|
24
|
+
if (typeof first !== "string") {
|
|
25
|
+
const message = typeof second === "string" ? second : "";
|
|
26
|
+
target[level](message, first);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (second === undefined) {
|
|
30
|
+
target[level](first);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (typeof second === "string") {
|
|
34
|
+
target[level](first, second);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
target[level](first, second);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface Logger {
|
|
41
|
+
trace(obj: LogBindings, msg?: string): void;
|
|
42
|
+
trace(msg: string, obj?: LogBindings): void;
|
|
43
|
+
debug(obj: LogBindings, msg?: string): void;
|
|
44
|
+
debug(msg: string, obj?: LogBindings): void;
|
|
45
|
+
info(obj: LogBindings, msg?: string): void;
|
|
46
|
+
info(msg: string, obj?: LogBindings): void;
|
|
47
|
+
warn(obj: LogBindings, msg?: string): void;
|
|
48
|
+
warn(msg: string, obj?: LogBindings): void;
|
|
49
|
+
error(obj: LogBindings, msg?: string): void;
|
|
50
|
+
error(msg: string, obj?: LogBindings): void;
|
|
51
|
+
fatal(obj: LogBindings, msg?: string): void;
|
|
52
|
+
fatal(msg: string, obj?: LogBindings): void;
|
|
53
|
+
child(bindings: LogBindings): Logger;
|
|
54
|
+
setBindings(bindings: LogBindings): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const createAdapter = (target: PowertoolsLogger): Logger => ({
|
|
58
|
+
trace: (first: LogBindings | string, second?: LogBindings | string): void =>
|
|
59
|
+
emit(target, "trace", first, second),
|
|
60
|
+
debug: (first: LogBindings | string, second?: LogBindings | string): void =>
|
|
61
|
+
emit(target, "debug", first, second),
|
|
62
|
+
info: (first: LogBindings | string, second?: LogBindings | string): void =>
|
|
63
|
+
emit(target, "info", first, second),
|
|
64
|
+
warn: (first: LogBindings | string, second?: LogBindings | string): void =>
|
|
65
|
+
emit(target, "warn", first, second),
|
|
66
|
+
error: (first: LogBindings | string, second?: LogBindings | string): void =>
|
|
67
|
+
emit(target, "error", first, second),
|
|
68
|
+
fatal: (first: LogBindings | string, second?: LogBindings | string): void =>
|
|
69
|
+
emit(target, "critical", first, second),
|
|
70
|
+
child: (bindings: LogBindings): Logger => {
|
|
71
|
+
const childLogger = target.createChild();
|
|
72
|
+
if (isBindings(bindings)) {
|
|
73
|
+
childLogger.appendPersistentKeys(bindings);
|
|
74
|
+
}
|
|
75
|
+
return createAdapter(childLogger);
|
|
76
|
+
},
|
|
77
|
+
setBindings: (bindings: LogBindings): void => {
|
|
78
|
+
target.appendPersistentKeys(bindings);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const powertoolsLogger = new PowertoolsLogger({
|
|
83
|
+
serviceName: process.env.POWERTOOLS_SERVICE_NAME ?? "remit",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const logger: Logger = createAdapter(powertoolsLogger);
|
|
87
|
+
|
|
88
|
+
export const metrics = new Metrics({
|
|
89
|
+
namespace: process.env.POWERTOOLS_METRICS_NAMESPACE ?? "Remit",
|
|
90
|
+
serviceName: process.env.POWERTOOLS_SERVICE_NAME ?? "remit",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const createLogger = (_context?: Context): Logger =>
|
|
94
|
+
createAdapter(powertoolsLogger);
|
|
95
|
+
|
|
96
|
+
export const withTelemetry = <TEvent, TResult>(
|
|
97
|
+
handler: (event: TEvent, context: Context) => Promise<TResult>,
|
|
98
|
+
): ((event: TEvent, context: Context) => Promise<TResult>) => {
|
|
99
|
+
return async (event: TEvent, context: Context): Promise<TResult> => {
|
|
100
|
+
powertoolsLogger.addContext(context);
|
|
101
|
+
logger.debug("Lambda invocation started", {
|
|
102
|
+
functionName: context.functionName,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
metrics.captureColdStartMetric();
|
|
106
|
+
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await handler(event, context);
|
|
111
|
+
const duration = Date.now() - start;
|
|
112
|
+
|
|
113
|
+
metrics.addMetric("invocationCount", MetricUnit.Count, 1);
|
|
114
|
+
metrics.addMetric("invocationLatency", MetricUnit.Milliseconds, duration);
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
metrics.addMetric("errorCount", MetricUnit.Count, 1);
|
|
119
|
+
logger.error("Lambda invocation failed", { error: String(err) });
|
|
120
|
+
throw err;
|
|
121
|
+
} finally {
|
|
122
|
+
metrics.publishStoredMetrics();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
};
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED