Skip to content

Commit 8d74dd2

Browse files
authored
feat(sdk): implement structured JSON logging for default logger (#304)
*Description of changes:* - Replace unstructured console.log output with proper JSON format - Fix default logger to output single JSON object per log entry - Use appropriate console methods (info, error, warn, debug) for each log level - Add TypeScript documentation with examples and circular reference warnings - Maintain backward compatibility with custom loggers (AWS Powertools, Winston, etc.) - Remove unnecessary fallback logic since context logger always provides structured data - Update tests to verify structured JSON output format This resolves the issue where logs were producing nested/duplicated strings instead of clean structured JSON that AWS Lambda can properly parse and log aggregators can query efficiently. *Issue #, if available:* #302
1 parent 6aca115 commit 8d74dd2

File tree

4 files changed

+131
-66
lines changed

4 files changed

+131
-66
lines changed

packages/aws-durable-execution-sdk-js/src/types/durable-context.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,43 @@ export interface DurableContext {
2828
* The underlying AWS Lambda context
2929
*/
3030
lambdaContext: Context;
31+
3132
/**
32-
* Logger instance for this context, enriched with execution context information
33+
* Logger instance for this context, automatically enriched with durable execution metadata.
34+
*
35+
* **Automatic Enrichment:**
36+
* All log entries are automatically enhanced with:
37+
* - `timestamp`: ISO timestamp of the log entry
38+
* - `execution_arn`: Durable execution ARN for tracing
39+
* - `step_id`: Current step identifier (when logging from within a step)
40+
* - `level`: Log level (info, error, warn, debug)
41+
* - `message`: The log message
42+
*
43+
* **Output Format:**
44+
* ```json
45+
* {
46+
* "timestamp": "2025-11-21T18:39:24.743Z",
47+
* "execution_arn": "arn:aws:lambda:...",
48+
* "level": "info",
49+
* "step_id": "abc123",
50+
* "message": "User action completed",
51+
* "data": { "userId": "123", "action": "login" }
52+
* }
53+
* ```
54+
*
55+
* @example
56+
* ```typescript
57+
* // Basic usage
58+
* context.logger.info("User logged in", { userId: "123" });
59+
*
60+
* // Error logging
61+
* context.logger.error("Database connection failed", error, { retryCount: 3 });
62+
*
63+
* // With custom logger (handles circular refs)
64+
* import { Logger } from '@aws-lambda-powertools/logger';
65+
* const powertoolsLogger = new Logger();
66+
* context.configureLogger({ customLogger: powertoolsLogger });
67+
* ```
3368
*/
3469
logger: Logger;
3570

packages/aws-durable-execution-sdk-js/src/types/logger.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,51 @@
11
/**
2-
* Generic logger interface for custom logger implementations
3-
* Provides structured logging capabilities for durable execution contexts
2+
* Generic logger interface for custom logger implementations.
3+
* Provides structured logging capabilities for durable execution contexts.
4+
*
5+
* When used through DurableContext, all log entries are automatically enriched
6+
* with execution metadata (timestamp, execution_arn, step_id, etc.).
47
*/
58
export interface Logger {
6-
/** Generic log method with configurable level (optional for compatibility with popular loggers) */
9+
/**
10+
* Generic log method with configurable level (optional for compatibility with popular loggers)
11+
* @param level - Log level (e.g., "info", "error", "warn", "debug")
12+
* @param message - Log message
13+
* @param data - Additional structured data to include in log entry
14+
* @param error - Error object (for error-level logs)
15+
*/
716
log?(level: string, message?: string, data?: unknown, error?: Error): void;
8-
/** Log error messages with optional error object and additional data */
17+
18+
/**
19+
* Log error messages with optional error object and additional data
20+
* @param message - Error description
21+
* @param error - Error object with stack trace
22+
* @param data - Additional context data
23+
* @example context.logger.error("Database query failed", dbError, \{ query: "SELECT * FROM users" \})
24+
*/
925
error(message?: string, error?: Error, data?: unknown): void;
10-
/** Log warning messages with optional additional data */
26+
27+
/**
28+
* Log warning messages with optional additional data
29+
* @param message - Warning message
30+
* @param data - Additional context data
31+
* @example context.logger.warn("Rate limit approaching", \{ currentRate: 95, limit: 100 \})
32+
*/
1133
warn(message?: string, data?: unknown): void;
12-
/** Log informational messages with optional additional data */
34+
35+
/**
36+
* Log informational messages with optional additional data
37+
* @param message - Information message
38+
* @param data - Additional context data
39+
* @example context.logger.info("User action completed", \{ userId: "123", action: "login" \})
40+
*/
1341
info(message?: string, data?: unknown): void;
14-
/** Log debug messages with optional additional data */
42+
43+
/**
44+
* Log debug messages with optional additional data
45+
* @param message - Debug message
46+
* @param data - Additional context data
47+
* @example context.logger.debug("Processing step", \{ stepName: "validation", duration: 150 \})
48+
*/
1549
debug(message?: string, data?: unknown): void;
1650
}
1751

Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import { createDefaultLogger } from "./default-logger";
22

33
describe("Default Logger", () => {
4-
let consoleSpy: jest.SpyInstance;
4+
let consoleSpies: {
5+
log: jest.SpyInstance;
6+
info: jest.SpyInstance;
7+
error: jest.SpyInstance;
8+
warn: jest.SpyInstance;
9+
debug: jest.SpyInstance;
10+
};
511

612
beforeEach(() => {
7-
consoleSpy = jest.spyOn(console, "log").mockImplementation();
13+
consoleSpies = {
14+
log: jest.spyOn(console, "log").mockImplementation(),
15+
info: jest.spyOn(console, "info").mockImplementation(),
16+
error: jest.spyOn(console, "error").mockImplementation(),
17+
warn: jest.spyOn(console, "warn").mockImplementation(),
18+
debug: jest.spyOn(console, "debug").mockImplementation(),
19+
};
820
});
921

1022
afterEach(() => {
11-
consoleSpy.mockRestore();
23+
Object.values(consoleSpies).forEach(spy => spy.mockRestore());
1224
});
1325

1426
it("should create a logger with all required methods", () => {
@@ -21,60 +33,35 @@ describe("Default Logger", () => {
2133
expect(logger).toHaveProperty("debug");
2234
});
2335

24-
it("should log with custom level and all parameters", () => {
36+
it("should output structured data using appropriate console methods", () => {
2537
const logger = createDefaultLogger();
26-
const testData = { key: "value" };
27-
const testError = new Error("test error");
38+
const structuredData = {
39+
timestamp: "2025-11-21T18:33:33.938Z",
40+
execution_arn: "test-arn",
41+
level: "info",
42+
step_id: "abc123",
43+
message: "structured message",
44+
};
2845

29-
logger.log?.("custom", "test message", testData, testError);
46+
logger.info("structured message", structuredData);
3047

31-
expect(consoleSpy).toHaveBeenCalledWith(
32-
"custom",
33-
"test message",
34-
testData,
35-
testError,
36-
);
48+
expect(consoleSpies.info).toHaveBeenCalledWith(structuredData);
3749
});
3850

39-
it("should log info messages", () => {
51+
it("should use correct console methods for each log level", () => {
4052
const logger = createDefaultLogger();
41-
const testData = { info: "data" };
53+
const testData = { test: "data" };
4254

55+
logger.log?.("custom", "test message", testData);
4356
logger.info("info message", testData);
44-
45-
expect(consoleSpy).toHaveBeenCalledWith("info", "info message", testData);
46-
});
47-
48-
it("should log error messages", () => {
49-
const logger = createDefaultLogger();
50-
const testError = new Error("test error");
51-
const testData = { error: "data" };
52-
53-
logger.error("error message", testError, testData);
54-
55-
expect(consoleSpy).toHaveBeenCalledWith(
56-
"error",
57-
"error message",
58-
testError,
59-
testData,
60-
);
61-
});
62-
63-
it("should log warn messages", () => {
64-
const logger = createDefaultLogger();
65-
const testData = { warn: "data" };
66-
57+
logger.error("error message", new Error("test"), testData);
6758
logger.warn("warn message", testData);
68-
69-
expect(consoleSpy).toHaveBeenCalledWith("warn", "warn message", testData);
70-
});
71-
72-
it("should log debug messages", () => {
73-
const logger = createDefaultLogger();
74-
const testData = { debug: "data" };
75-
7659
logger.debug("debug message", testData);
7760

78-
expect(consoleSpy).toHaveBeenCalledWith("debug", "debug message", testData);
61+
expect(consoleSpies.log).toHaveBeenCalledWith(testData);
62+
expect(consoleSpies.info).toHaveBeenCalledWith(testData);
63+
expect(consoleSpies.error).toHaveBeenCalledWith(testData);
64+
expect(consoleSpies.warn).toHaveBeenCalledWith(testData);
65+
expect(consoleSpies.debug).toHaveBeenCalledWith(testData);
7966
});
8067
});
Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
import { Logger } from "../../types";
22

33
/**
4-
* Creates a default logger that outputs to console.
4+
* Creates a default logger that outputs structured logs to console.
55
* Used as fallback when no custom logger is provided.
6+
* Always expects structured data from context logger.
7+
*
8+
* Note: _message parameters are unused because the message is already included
9+
* in the structured data object. Parameters are kept for Logger interface compatibility.
610
*/
711
/* eslint-disable no-console */
812
export const createDefaultLogger = (): Logger => ({
9-
log: (level: string, message?: string, data?: unknown, error?: Error) =>
10-
console.log(level, message, data, error),
11-
info: (message?: string, data?: unknown) =>
12-
console.log("info", message, data),
13-
error: (message?: string, error?: Error, data?: unknown) =>
14-
console.log("error", message, error, data),
15-
warn: (message?: string, data?: unknown) =>
16-
console.log("warn", message, data),
17-
debug: (message?: string, data?: unknown) =>
18-
console.log("debug", message, data),
13+
log: (_level: string, _message?: string, data?: unknown, _error?: Error): void => {
14+
console.log(data);
15+
},
16+
info: (_message?: string, data?: unknown): void => {
17+
console.info(data);
18+
},
19+
error: (_message?: string, _error?: Error, data?: unknown): void => {
20+
console.error(data);
21+
},
22+
warn: (_message?: string, data?: unknown): void => {
23+
console.warn(data);
24+
},
25+
debug: (_message?: string, data?: unknown): void => {
26+
console.debug(data);
27+
},
1928
});
2029
/* eslint-enable no-console */

0 commit comments

Comments
 (0)