@vitkuz/aws-logger 1.3.0 → 1.5.0
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/.prettierignore +4 -3
- package/.prettierrc.json +8 -0
- package/dist/index.d.mts +7 -8
- package/dist/index.d.ts +7 -8
- package/dist/index.js +47 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +46 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -4
- package/src/context.ts +11 -7
- package/src/logger.ts +36 -15
- package/src/types.ts +5 -1
- package/src/wrapper.ts +24 -20
- package/tsconfig.json +2 -5
package/.prettierignore
CHANGED
package/.prettierrc.json
ADDED
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ interface Logger {
|
|
|
2
2
|
debug(message: string, context?: Record<string, any>): void;
|
|
3
3
|
info(message: string, context?: Record<string, any>): void;
|
|
4
4
|
warn(message: string, context?: Record<string, any>): void;
|
|
5
|
-
error(message: string,
|
|
5
|
+
error(message: string, errorOrContext?: Error | string | Record<string, any>, context?: Record<string, any>): void;
|
|
6
6
|
/**
|
|
7
7
|
* Creates a child logger with bound context.
|
|
8
8
|
* Any logs emitted by the child will include the parent's context plus the new context.
|
|
@@ -39,16 +39,15 @@ declare const createRedactor: (config: RedactionConfig) => (info: any) => any;
|
|
|
39
39
|
* Runs a callback within a logger context.
|
|
40
40
|
* The logger provided becomes the "current" logger for the duration of the callback.
|
|
41
41
|
*/
|
|
42
|
-
declare const runWithLogger: <T>(logger: Logger, callback: () => T) => T;
|
|
42
|
+
declare const runWithLogger: <T>(logger: Logger, callback: () => T, requestId?: string) => T;
|
|
43
43
|
/**
|
|
44
44
|
* Gets the current logger from the async context.
|
|
45
|
-
* Throws an error if called outside of a runWithLogger context,
|
|
46
|
-
* unless a fallback is provided (though usually we want to enforce context).
|
|
47
|
-
*
|
|
48
|
-
* To make it easier to use, we can return undefined or a default logger if needed,
|
|
49
|
-
* but for this specific feature request "access by all down the stream", strictness is good.
|
|
50
45
|
*/
|
|
51
46
|
declare const getLogger: () => Logger | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Gets the current request ID from the async context.
|
|
49
|
+
*/
|
|
50
|
+
declare const getRequestId: () => string | undefined;
|
|
52
51
|
/**
|
|
53
52
|
* Updates the current context's logger.
|
|
54
53
|
* This effectively "extends" the logger for the remainder of the current async execution
|
|
@@ -86,4 +85,4 @@ declare const withLogger: <TEvent = any, TResult = any>(handler: Handler<TEvent,
|
|
|
86
85
|
declare const DEFAULT_LOG_LEVEL = "info";
|
|
87
86
|
declare const REQUEST_ID_KEY = "x-request-id";
|
|
88
87
|
|
|
89
|
-
export { COMMON_REDACTION_KEYS, type CreateLoggerOptions, DEFAULT_LOG_LEVEL, type Logger, REQUEST_ID_KEY, type RedactionConfig, type RedactionStrategy, createLogger, createRedactor, getLogger, recursiveRedact, runWithLogger, updateLoggerContext, withLogger };
|
|
88
|
+
export { COMMON_REDACTION_KEYS, type CreateLoggerOptions, DEFAULT_LOG_LEVEL, type Logger, REQUEST_ID_KEY, type RedactionConfig, type RedactionStrategy, createLogger, createRedactor, getLogger, getRequestId, recursiveRedact, runWithLogger, updateLoggerContext, withLogger };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ interface Logger {
|
|
|
2
2
|
debug(message: string, context?: Record<string, any>): void;
|
|
3
3
|
info(message: string, context?: Record<string, any>): void;
|
|
4
4
|
warn(message: string, context?: Record<string, any>): void;
|
|
5
|
-
error(message: string,
|
|
5
|
+
error(message: string, errorOrContext?: Error | string | Record<string, any>, context?: Record<string, any>): void;
|
|
6
6
|
/**
|
|
7
7
|
* Creates a child logger with bound context.
|
|
8
8
|
* Any logs emitted by the child will include the parent's context plus the new context.
|
|
@@ -39,16 +39,15 @@ declare const createRedactor: (config: RedactionConfig) => (info: any) => any;
|
|
|
39
39
|
* Runs a callback within a logger context.
|
|
40
40
|
* The logger provided becomes the "current" logger for the duration of the callback.
|
|
41
41
|
*/
|
|
42
|
-
declare const runWithLogger: <T>(logger: Logger, callback: () => T) => T;
|
|
42
|
+
declare const runWithLogger: <T>(logger: Logger, callback: () => T, requestId?: string) => T;
|
|
43
43
|
/**
|
|
44
44
|
* Gets the current logger from the async context.
|
|
45
|
-
* Throws an error if called outside of a runWithLogger context,
|
|
46
|
-
* unless a fallback is provided (though usually we want to enforce context).
|
|
47
|
-
*
|
|
48
|
-
* To make it easier to use, we can return undefined or a default logger if needed,
|
|
49
|
-
* but for this specific feature request "access by all down the stream", strictness is good.
|
|
50
45
|
*/
|
|
51
46
|
declare const getLogger: () => Logger | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Gets the current request ID from the async context.
|
|
49
|
+
*/
|
|
50
|
+
declare const getRequestId: () => string | undefined;
|
|
52
51
|
/**
|
|
53
52
|
* Updates the current context's logger.
|
|
54
53
|
* This effectively "extends" the logger for the remainder of the current async execution
|
|
@@ -86,4 +85,4 @@ declare const withLogger: <TEvent = any, TResult = any>(handler: Handler<TEvent,
|
|
|
86
85
|
declare const DEFAULT_LOG_LEVEL = "info";
|
|
87
86
|
declare const REQUEST_ID_KEY = "x-request-id";
|
|
88
87
|
|
|
89
|
-
export { COMMON_REDACTION_KEYS, type CreateLoggerOptions, DEFAULT_LOG_LEVEL, type Logger, REQUEST_ID_KEY, type RedactionConfig, type RedactionStrategy, createLogger, createRedactor, getLogger, recursiveRedact, runWithLogger, updateLoggerContext, withLogger };
|
|
88
|
+
export { COMMON_REDACTION_KEYS, type CreateLoggerOptions, DEFAULT_LOG_LEVEL, type Logger, REQUEST_ID_KEY, type RedactionConfig, type RedactionStrategy, createLogger, createRedactor, getLogger, getRequestId, recursiveRedact, runWithLogger, updateLoggerContext, withLogger };
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
createLogger: () => createLogger,
|
|
37
37
|
createRedactor: () => createRedactor,
|
|
38
38
|
getLogger: () => getLogger,
|
|
39
|
+
getRequestId: () => getRequestId,
|
|
39
40
|
recursiveRedact: () => recursiveRedact,
|
|
40
41
|
runWithLogger: () => runWithLogger,
|
|
41
42
|
updateLoggerContext: () => updateLoggerContext,
|
|
@@ -139,13 +140,17 @@ var createRedactor = (config) => {
|
|
|
139
140
|
// src/context.ts
|
|
140
141
|
var import_async_hooks = require("async_hooks");
|
|
141
142
|
var asyncLocalStorage = new import_async_hooks.AsyncLocalStorage();
|
|
142
|
-
var runWithLogger = (logger, callback) => {
|
|
143
|
-
return asyncLocalStorage.run({ logger }, callback);
|
|
143
|
+
var runWithLogger = (logger, callback, requestId) => {
|
|
144
|
+
return asyncLocalStorage.run({ logger, requestId }, callback);
|
|
144
145
|
};
|
|
145
146
|
var getLogger = () => {
|
|
146
147
|
const store = asyncLocalStorage.getStore();
|
|
147
148
|
return store?.logger;
|
|
148
149
|
};
|
|
150
|
+
var getRequestId = () => {
|
|
151
|
+
const store = asyncLocalStorage.getStore();
|
|
152
|
+
return store?.requestId;
|
|
153
|
+
};
|
|
149
154
|
var updateLoggerContext = (newLogger) => {
|
|
150
155
|
const store = asyncLocalStorage.getStore();
|
|
151
156
|
if (store) {
|
|
@@ -179,18 +184,32 @@ var createLoggerWrapper = (winstonLogger) => {
|
|
|
179
184
|
warn: (message, context) => {
|
|
180
185
|
winstonLogger.warn(message, formatContext(context));
|
|
181
186
|
},
|
|
182
|
-
error: (message,
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
...rest
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
error: (message, errorOrContext, context) => {
|
|
188
|
+
let meta = formatContext(context);
|
|
189
|
+
if (errorOrContext instanceof Error || typeof errorOrContext === "string") {
|
|
190
|
+
const error = errorOrContext;
|
|
191
|
+
if (error instanceof Error) {
|
|
192
|
+
const { message: message2, name, stack, ...rest } = error;
|
|
193
|
+
meta.error = {
|
|
194
|
+
...rest,
|
|
195
|
+
message: message2,
|
|
196
|
+
name,
|
|
197
|
+
stack
|
|
198
|
+
};
|
|
199
|
+
} else {
|
|
200
|
+
meta.error = error;
|
|
201
|
+
}
|
|
202
|
+
} else if (typeof errorOrContext === "object" && errorOrContext !== null) {
|
|
203
|
+
meta = { ...meta, ...errorOrContext };
|
|
204
|
+
if (meta.error instanceof Error) {
|
|
205
|
+
const { message: message2, name, stack, ...rest } = meta.error;
|
|
206
|
+
meta.error = {
|
|
207
|
+
...rest,
|
|
208
|
+
message: message2,
|
|
209
|
+
name,
|
|
210
|
+
stack
|
|
211
|
+
};
|
|
212
|
+
}
|
|
194
213
|
}
|
|
195
214
|
winstonLogger.error(message, meta);
|
|
196
215
|
},
|
|
@@ -235,15 +254,19 @@ var withLogger = (handler, options = {}) => {
|
|
|
235
254
|
const scopedLogger = rootLogger.child(requestContext);
|
|
236
255
|
scopedLogger.debug("Lambda Event", { event });
|
|
237
256
|
scopedLogger.debug("Lambda Context", { context });
|
|
238
|
-
return runWithLogger(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
257
|
+
return runWithLogger(
|
|
258
|
+
scopedLogger,
|
|
259
|
+
async () => {
|
|
260
|
+
try {
|
|
261
|
+
const result = await handler(event, context, callback);
|
|
262
|
+
return result;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
scopedLogger.error("Unhandled Lambda Exception", error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
requestContext.requestId
|
|
269
|
+
);
|
|
247
270
|
};
|
|
248
271
|
};
|
|
249
272
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -254,6 +277,7 @@ var withLogger = (handler, options = {}) => {
|
|
|
254
277
|
createLogger,
|
|
255
278
|
createRedactor,
|
|
256
279
|
getLogger,
|
|
280
|
+
getRequestId,
|
|
257
281
|
recursiveRedact,
|
|
258
282
|
runWithLogger,
|
|
259
283
|
updateLoggerContext,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/redactor.ts","../src/context.ts","../src/logger.ts","../src/constants.ts","../src/wrapper.ts"],"sourcesContent":["export * from './types';\nexport * from './redactor';\nexport * from './context';\nexport * from './logger';\nexport * from './wrapper';\nexport * from './constants';\n","import crypto from 'crypto';\n\nexport type RedactionStrategy = 'mask' | 'remove' | 'hash' | string | ((value: any) => any);\n\nexport interface RedactionConfig {\n /**\n * Array of keys to redact.\n * Case-insensitive matching is recommended/implemented.\n */\n keys: string[];\n /**\n * Map of specific key to strategy.\n * If a key is in 'keys' but not here, it uses the default strategy ('mask').\n */\n strategies?: Record<string, RedactionStrategy>;\n /**\n * Default strategy for keys found in the list but not in the strategy map.\n * Default: 'mask'\n */\n defaultStrategy?: RedactionStrategy;\n}\n\nexport const COMMON_REDACTION_KEYS = [\n 'password',\n 'passwd',\n 'secret',\n 'token',\n 'access_token',\n 'access-token',\n 'auth',\n 'auth-token',\n 'authorization',\n 'apikey',\n 'api_key',\n 'api-key',\n 'card',\n 'cvv',\n 'ssn',\n 'pin',\n];\n\n/**\n * Applies the redaction strategy to a single value.\n */\nconst applyStrategy = (value: any, strategy: RedactionStrategy): any => {\n if (typeof strategy === 'function') {\n return strategy(value);\n }\n\n if (typeof strategy === 'string' && strategy.startsWith('mask-last-')) {\n const parts = strategy.split('-');\n const lastN = parseInt(parts[2], 10);\n\n if (typeof value === 'string' && !isNaN(lastN)) {\n if (value.length <= lastN) {\n return value; // Or mask entirely? Usually if shorter, we show it or mask all. Let's return as is or mask all?\n // Security wise: showing short secrets fully is bad. Masking all is safer.\n // BUT, if user asked for last 4 and length is 3, showing 3 is effectively \"last 4\".\n // Let's mask all if length <= lastN to be safe? Or just return.\n // Actually, standard is usually: if len <= N, show all (it IS the last N).\n // But for API keys, usually we want to see SOMETHING masked.\n // Let's preserve standard behavior: visible suffix.\n return value;\n }\n const maskLen = value.length - lastN;\n return '*'.repeat(maskLen) + value.slice(-lastN);\n }\n return '*****'; // Fallback for non-strings\n }\n\n switch (strategy) {\n case 'remove':\n return undefined;\n case 'hash':\n if (typeof value === 'string' || typeof value === 'number') {\n return crypto.createHash('sha256').update(String(value)).digest('hex');\n }\n return '[HASH_FAILED_TYPE]';\n case 'mask':\n default:\n return '*****';\n }\n};\n\n/**\n * Recursively walks the object and redacts fields matching the config.\n */\nexport const recursiveRedact = (\n target: any,\n config: RedactionConfig,\n cache = new Set<any>(),\n): any => {\n if (target === null || typeof target !== 'object') {\n return target;\n }\n\n // Handle circular references\n if (cache.has(target)) {\n return '[Circular]';\n }\n cache.add(target);\n\n // Handle Arrays\n if (Array.isArray(target)) {\n return target.map((item) => recursiveRedact(item, config, cache));\n }\n\n // Handle Objects\n const redactedObj: Record<string, any> = {};\n const keysToRedact = new Set(config.keys.map((k) => k.toLowerCase()));\n\n for (const [key, value] of Object.entries(target)) {\n const lowerKey = key.toLowerCase();\n\n if (keysToRedact.has(lowerKey)) {\n // Determine strategy\n let strategy = config.defaultStrategy || 'mask';\n if (config.strategies && config.strategies[key]) {\n // Try exact match first\n strategy = config.strategies[key];\n } else if (config.strategies) {\n // Try case-insensitive lookup in strategies if needed,\n // but for simplicity let's rely on specific keys matching or default.\n // Actually, user might put 'Password': 'hash' and we matched 'password'.\n // Let's iterate strategies keys to find case-insensitive match if strictly needed.\n // For performance/simplicity, strict case matching in strategies map is safer,\n // but generic 'keys' list is case-insensitive.\n\n // Let's look for a key in strategies that matches lowerKey\n const strategyKey = Object.keys(config.strategies).find(\n (k) => k.toLowerCase() === lowerKey,\n );\n if (strategyKey) {\n strategy = config.strategies[strategyKey];\n }\n }\n\n const result = applyStrategy(value, strategy);\n if (result !== undefined) {\n redactedObj[key] = result;\n }\n // If undefined (remove strategy), we just don't add the key.\n } else {\n // Recurse\n redactedObj[key] = recursiveRedact(value, config, cache);\n }\n }\n\n return redactedObj;\n};\n\nexport const createRedactor = (config: RedactionConfig) => {\n return (info: any) => {\n return recursiveRedact(info, config);\n };\n};\n","import { AsyncLocalStorage } from 'async_hooks';\nimport { Logger } from './types';\n\n// We store a reference to the logger so we can update it in-place for the current context\ninterface LoggerStore {\n logger: Logger;\n}\n\nconst asyncLocalStorage = new AsyncLocalStorage<LoggerStore>();\n\n/**\n * Runs a callback within a logger context.\n * The logger provided becomes the \"current\" logger for the duration of the callback.\n */\nexport const runWithLogger = <T>(logger: Logger, callback: () => T): T => {\n return asyncLocalStorage.run({ logger }, callback);\n};\n\n/**\n * Gets the current logger from the async context.\n * Throws an error if called outside of a runWithLogger context,\n * unless a fallback is provided (though usually we want to enforce context).\n *\n * To make it easier to use, we can return undefined or a default logger if needed,\n * but for this specific feature request \"access by all down the stream\", strictness is good.\n */\nexport const getLogger = (): Logger | undefined => {\n const store = asyncLocalStorage.getStore();\n return store?.logger;\n};\n\n/**\n * Updates the current context's logger.\n * This effectively \"extends\" the logger for the remainder of the current async execution\n * and any further downstream calls sharing this context.\n */\nexport const updateLoggerContext = (newLogger: Logger): void => {\n const store = asyncLocalStorage.getStore();\n if (store) {\n store.logger = newLogger;\n } else {\n // If we are not in a context, we can't update it.\n // We could throw, or warn. For now, let's warn.\n console.warn(\n 'AntigravityLogger: updateLoggerContext called outside of an active context. Logic ignored.',\n );\n }\n};\n","import winston from 'winston';\nimport { Logger } from './types';\nimport { DEFAULT_LOG_LEVEL } from './constants';\nimport { RedactionConfig, createRedactor } from './redactor';\n\nexport interface CreateLoggerOptions {\n /**\n * Default context to include in all logs\n */\n defaultContext?: Record<string, any>;\n /**\n * Log level (default: process.env.LOG_LEVEL || 'info')\n */\n level?: string;\n /**\n * Configuration for data redaction\n */\n redaction?: RedactionConfig;\n}\n\nconst formatContext = (context?: Record<string, any>): Record<string, any> => {\n return context || {};\n};\n\nconst createLoggerWrapper = (winstonLogger: winston.Logger): Logger => {\n return {\n debug: (message: string, context?: Record<string, any>): void => {\n winstonLogger.debug(message, formatContext(context));\n },\n\n info: (message: string, context?: Record<string, any>): void => {\n winstonLogger.info(message, formatContext(context));\n },\n\n warn: (message: string, context?: Record<string, any>): void => {\n winstonLogger.warn(message, formatContext(context));\n },\n\n error: (message: string, error?: Error | string, context?: Record<string, any>): void => {\n const meta = formatContext(context);\n if (error instanceof Error) {\n // Winston handles error objects well if passed as meta or splat\n // But for explicit structure, let's attach it\n // Spread error first to capture custom props, then overwrite standard ones to ensure presence\n const { message, name, stack, ...rest } = error;\n meta.error = {\n ...rest,\n message,\n name,\n stack,\n };\n } else if (error) {\n meta.error = error;\n }\n winstonLogger.error(message, meta);\n },\n\n child: (context: Record<string, any>): Logger => {\n // Winston's child() returns a new logger instance with the metadata bound\n return createLoggerWrapper(winstonLogger.child(context));\n },\n };\n};\n\nexport const createLogger = (options: CreateLoggerOptions = {}): Logger => {\n const level = options.level || process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL;\n\n // Create redactor if config is present\n const redactor = options.redaction ? createRedactor(options.redaction) : null;\n\n // Custom format that applies redaction\n const redactionFormat = winston.format((info) => {\n if (redactor) {\n return redactor(info);\n }\n return info;\n });\n\n const format = winston.format.combine(\n winston.format.timestamp(),\n redactionFormat(), // Apply redaction before JSON\n winston.format.json(),\n );\n\n const winstonLogger = winston.createLogger({\n level,\n format,\n defaultMeta: options.defaultContext,\n transports: [new winston.transports.Console()],\n });\n\n return createLoggerWrapper(winstonLogger);\n};\n","export const DEFAULT_LOG_LEVEL = 'info';\nexport const REQUEST_ID_KEY = 'x-request-id';\n","import { CreateLoggerOptions, createLogger } from './logger';\nimport { runWithLogger } from './context';\n\n// Generic Handler type compatible with AWS Lambda\n// We use 'any' to avoid strict dependency on @types/aws-lambda for this generic wrapper\n// but in practice it wraps (event, context, callback?) => Promise<any> | any\ntype Handler<TEvent = any, TResult = any> = (\n event: TEvent,\n context: any,\n callback?: any,\n) => Promise<TResult> | void;\n\n/**\n * Higher-order function to wrap a Lambda handler with logger context.\n * Automatically extracts 'awsRequestId' and 'functionName' from the Lambda context\n * and initializes a logger for the request scope.\n *\n * @param handler The original Lambda handler\n * @param options Logger options (level, redaction, etc.)\n */\nexport const withLogger = <TEvent = any, TResult = any>(\n handler: Handler<TEvent, TResult>,\n options: CreateLoggerOptions = {},\n): Handler<TEvent, TResult> => {\n return async (event: TEvent, context: any, callback?: any) => {\n // 1. Initialize Logger\n // Create a root logger if custom options are provided, or use default behavior.\n // We create a FRESH logger instance for each request to ensure context isolation if needed?\n // Actually, creating a logger instance is cheap?\n // Winston createLogger is relatively heavy.\n // Optimization: We could reuse a global logger and just child() it.\n // But options might change? Usually options are static.\n // Let's assume options are static per lambda wrapper usage.\n\n // HOWEVER, to support dynamic options per request might be overkill.\n // Let's create one global logger instance for this wrapper instantiation if possible?\n // But wait, the standard `createLogger` we implemented creates a NEW winston instance every time.\n // We should probably cache it?\n // For now, let's keep it simple: createLogger per request.\n // If performance becomes an issue, we can refactor `createLogger` to reuse the winston instance if options match.\n\n const rootLogger = createLogger(options);\n\n // 2. Extract Context\n const requestContext: Record<string, any> = {};\n if (context) {\n if (context.awsRequestId) requestContext.requestId = context.awsRequestId;\n if (context.functionName) requestContext.functionName = context.functionName;\n }\n\n // 3. Create Child Logger with Request Context\n const scopedLogger = rootLogger.child(requestContext);\n\n // 3a. Log Event and Context\n scopedLogger.debug('Lambda Event', { event });\n scopedLogger.debug('Lambda Context', { context });\n\n // 4. Run Handler in Context\n return runWithLogger(scopedLogger, async () => {\n // We use 'await' to ensure the context stays active during the handler execution\n // If the handler accepts a callback, we might need special handling?\n // Most modern lambdas use async/await.\n // If legacy callback style is used, AsyncLocalStorage context *should* still propagate\n // if the callback is invoked asynchronously.\n // But we wrap the result in Promise usually.\n\n // Supporting both async and callback style:\n try {\n // If it returns a promise, await it\n const result = await handler(event, context, callback);\n return result as TResult;\n } catch (error) {\n // We could log unhandled errors here too?\n // Standard lambda practice is to let the error propagate so Lambda runtime sees it (and retries etc)\n // BUT we should log it first because once it leaves here, we might lose the logger context behavior.\n scopedLogger.error('Unhandled Lambda Exception', error as Error);\n throw error;\n }\n });\n };\n};\n"],"mappings":"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","names":["crypto","message","winston"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/redactor.ts","../src/context.ts","../src/logger.ts","../src/constants.ts","../src/wrapper.ts"],"sourcesContent":["export * from './types';\nexport * from './redactor';\nexport * from './context';\nexport * from './logger';\nexport * from './wrapper';\nexport * from './constants';\n","import crypto from 'crypto';\n\nexport type RedactionStrategy = 'mask' | 'remove' | 'hash' | string | ((value: any) => any);\n\nexport interface RedactionConfig {\n /**\n * Array of keys to redact.\n * Case-insensitive matching is recommended/implemented.\n */\n keys: string[];\n /**\n * Map of specific key to strategy.\n * If a key is in 'keys' but not here, it uses the default strategy ('mask').\n */\n strategies?: Record<string, RedactionStrategy>;\n /**\n * Default strategy for keys found in the list but not in the strategy map.\n * Default: 'mask'\n */\n defaultStrategy?: RedactionStrategy;\n}\n\nexport const COMMON_REDACTION_KEYS = [\n 'password',\n 'passwd',\n 'secret',\n 'token',\n 'access_token',\n 'access-token',\n 'auth',\n 'auth-token',\n 'authorization',\n 'apikey',\n 'api_key',\n 'api-key',\n 'card',\n 'cvv',\n 'ssn',\n 'pin',\n];\n\n/**\n * Applies the redaction strategy to a single value.\n */\nconst applyStrategy = (value: any, strategy: RedactionStrategy): any => {\n if (typeof strategy === 'function') {\n return strategy(value);\n }\n\n if (typeof strategy === 'string' && strategy.startsWith('mask-last-')) {\n const parts = strategy.split('-');\n const lastN = parseInt(parts[2], 10);\n\n if (typeof value === 'string' && !isNaN(lastN)) {\n if (value.length <= lastN) {\n return value; // Or mask entirely? Usually if shorter, we show it or mask all. Let's return as is or mask all?\n // Security wise: showing short secrets fully is bad. Masking all is safer.\n // BUT, if user asked for last 4 and length is 3, showing 3 is effectively \"last 4\".\n // Let's mask all if length <= lastN to be safe? Or just return.\n // Actually, standard is usually: if len <= N, show all (it IS the last N).\n // But for API keys, usually we want to see SOMETHING masked.\n // Let's preserve standard behavior: visible suffix.\n return value;\n }\n const maskLen = value.length - lastN;\n return '*'.repeat(maskLen) + value.slice(-lastN);\n }\n return '*****'; // Fallback for non-strings\n }\n\n switch (strategy) {\n case 'remove':\n return undefined;\n case 'hash':\n if (typeof value === 'string' || typeof value === 'number') {\n return crypto.createHash('sha256').update(String(value)).digest('hex');\n }\n return '[HASH_FAILED_TYPE]';\n case 'mask':\n default:\n return '*****';\n }\n};\n\n/**\n * Recursively walks the object and redacts fields matching the config.\n */\nexport const recursiveRedact = (\n target: any,\n config: RedactionConfig,\n cache = new Set<any>(),\n): any => {\n if (target === null || typeof target !== 'object') {\n return target;\n }\n\n // Handle circular references\n if (cache.has(target)) {\n return '[Circular]';\n }\n cache.add(target);\n\n // Handle Arrays\n if (Array.isArray(target)) {\n return target.map((item) => recursiveRedact(item, config, cache));\n }\n\n // Handle Objects\n const redactedObj: Record<string, any> = {};\n const keysToRedact = new Set(config.keys.map((k) => k.toLowerCase()));\n\n for (const [key, value] of Object.entries(target)) {\n const lowerKey = key.toLowerCase();\n\n if (keysToRedact.has(lowerKey)) {\n // Determine strategy\n let strategy = config.defaultStrategy || 'mask';\n if (config.strategies && config.strategies[key]) {\n // Try exact match first\n strategy = config.strategies[key];\n } else if (config.strategies) {\n // Try case-insensitive lookup in strategies if needed,\n // but for simplicity let's rely on specific keys matching or default.\n // Actually, user might put 'Password': 'hash' and we matched 'password'.\n // Let's iterate strategies keys to find case-insensitive match if strictly needed.\n // For performance/simplicity, strict case matching in strategies map is safer,\n // but generic 'keys' list is case-insensitive.\n\n // Let's look for a key in strategies that matches lowerKey\n const strategyKey = Object.keys(config.strategies).find(\n (k) => k.toLowerCase() === lowerKey,\n );\n if (strategyKey) {\n strategy = config.strategies[strategyKey];\n }\n }\n\n const result = applyStrategy(value, strategy);\n if (result !== undefined) {\n redactedObj[key] = result;\n }\n // If undefined (remove strategy), we just don't add the key.\n } else {\n // Recurse\n redactedObj[key] = recursiveRedact(value, config, cache);\n }\n }\n\n return redactedObj;\n};\n\nexport const createRedactor = (config: RedactionConfig) => {\n return (info: any) => {\n return recursiveRedact(info, config);\n };\n};\n","import { AsyncLocalStorage } from 'async_hooks';\nimport { Logger } from './types';\n\n// We store a reference to the logger so we can update it in-place for the current context\ninterface LoggerStore {\n logger: Logger;\n requestId?: string;\n}\n\nconst asyncLocalStorage = new AsyncLocalStorage<LoggerStore>();\n\n/**\n * Runs a callback within a logger context.\n * The logger provided becomes the \"current\" logger for the duration of the callback.\n */\nexport const runWithLogger = <T>(logger: Logger, callback: () => T, requestId?: string): T => {\n return asyncLocalStorage.run({ logger, requestId }, callback);\n};\n\n/**\n * Gets the current logger from the async context.\n */\nexport const getLogger = (): Logger | undefined => {\n const store = asyncLocalStorage.getStore();\n return store?.logger;\n};\n\n/**\n * Gets the current request ID from the async context.\n */\nexport const getRequestId = (): string | undefined => {\n const store = asyncLocalStorage.getStore();\n return store?.requestId;\n};\n\n/**\n * Updates the current context's logger.\n * This effectively \"extends\" the logger for the remainder of the current async execution\n * and any further downstream calls sharing this context.\n */\nexport const updateLoggerContext = (newLogger: Logger): void => {\n const store = asyncLocalStorage.getStore();\n if (store) {\n store.logger = newLogger;\n } else {\n // If we are not in a context, we can't update it.\n // We could throw, or warn. For now, let's warn.\n console.warn(\n 'AntigravityLogger: updateLoggerContext called outside of an active context. Logic ignored.',\n );\n }\n};\n","import winston from 'winston';\nimport { Logger } from './types';\nimport { DEFAULT_LOG_LEVEL } from './constants';\nimport { RedactionConfig, createRedactor } from './redactor';\n\nexport interface CreateLoggerOptions {\n /**\n * Default context to include in all logs\n */\n defaultContext?: Record<string, any>;\n /**\n * Log level (default: process.env.LOG_LEVEL || 'info')\n */\n level?: string;\n /**\n * Configuration for data redaction\n */\n redaction?: RedactionConfig;\n}\n\nconst formatContext = (context?: Record<string, any>): Record<string, any> => {\n return context || {};\n};\n\nconst createLoggerWrapper = (winstonLogger: winston.Logger): Logger => {\n return {\n debug: (message: string, context?: Record<string, any>): void => {\n winstonLogger.debug(message, formatContext(context));\n },\n\n info: (message: string, context?: Record<string, any>): void => {\n winstonLogger.info(message, formatContext(context));\n },\n\n warn: (message: string, context?: Record<string, any>): void => {\n winstonLogger.warn(message, formatContext(context));\n },\n\n error: (\n message: string,\n errorOrContext?: Error | string | Record<string, any>,\n context?: Record<string, any>,\n ): void => {\n let meta = formatContext(context);\n\n if (errorOrContext instanceof Error || typeof errorOrContext === 'string') {\n // Old behavior: second arg is the error\n const error = errorOrContext;\n if (error instanceof Error) {\n const { message, name, stack, ...rest } = error;\n meta.error = {\n ...rest,\n message,\n name,\n stack,\n };\n } else {\n meta.error = error;\n }\n } else if (typeof errorOrContext === 'object' && errorOrContext !== null) {\n // New behavior: second arg is context (which might contain error)\n meta = { ...meta, ...errorOrContext };\n\n // If error is in the context, format it nicely\n if (meta.error instanceof Error) {\n const { message, name, stack, ...rest } = meta.error;\n meta.error = {\n ...rest,\n message,\n name,\n stack,\n };\n }\n }\n\n winstonLogger.error(message, meta);\n },\n\n child: (context: Record<string, any>): Logger => {\n // Winston's child() returns a new logger instance with the metadata bound\n return createLoggerWrapper(winstonLogger.child(context));\n },\n };\n};\n\nexport const createLogger = (options: CreateLoggerOptions = {}): Logger => {\n const level = options.level || process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL;\n\n // Create redactor if config is present\n const redactor = options.redaction ? createRedactor(options.redaction) : null;\n\n // Custom format that applies redaction\n const redactionFormat = winston.format((info) => {\n if (redactor) {\n return redactor(info);\n }\n return info;\n });\n\n const format = winston.format.combine(\n winston.format.timestamp(),\n redactionFormat(), // Apply redaction before JSON\n winston.format.json(),\n );\n\n const winstonLogger = winston.createLogger({\n level,\n format,\n defaultMeta: options.defaultContext,\n transports: [new winston.transports.Console()],\n });\n\n return createLoggerWrapper(winstonLogger);\n};\n","export const DEFAULT_LOG_LEVEL = 'info';\nexport const REQUEST_ID_KEY = 'x-request-id';\n","import { CreateLoggerOptions, createLogger } from './logger';\nimport { runWithLogger } from './context';\n\n// Generic Handler type compatible with AWS Lambda\n// We use 'any' to avoid strict dependency on @types/aws-lambda for this generic wrapper\n// but in practice it wraps (event, context, callback?) => Promise<any> | any\ntype Handler<TEvent = any, TResult = any> = (\n event: TEvent,\n context: any,\n callback?: any,\n) => Promise<TResult> | void;\n\n/**\n * Higher-order function to wrap a Lambda handler with logger context.\n * Automatically extracts 'awsRequestId' and 'functionName' from the Lambda context\n * and initializes a logger for the request scope.\n *\n * @param handler The original Lambda handler\n * @param options Logger options (level, redaction, etc.)\n */\nexport const withLogger = <TEvent = any, TResult = any>(\n handler: Handler<TEvent, TResult>,\n options: CreateLoggerOptions = {},\n): Handler<TEvent, TResult> => {\n return async (event: TEvent, context: any, callback?: any) => {\n // 1. Initialize Logger\n // Create a root logger if custom options are provided, or use default behavior.\n // We create a FRESH logger instance for each request to ensure context isolation if needed?\n // Actually, creating a logger instance is cheap?\n // Winston createLogger is relatively heavy.\n // Optimization: We could reuse a global logger and just child() it.\n // But options might change? Usually options are static.\n // Let's assume options are static per lambda wrapper usage.\n\n // HOWEVER, to support dynamic options per request might be overkill.\n // Let's create one global logger instance for this wrapper instantiation if possible?\n // But wait, the standard `createLogger` we implemented creates a NEW winston instance every time.\n // We should probably cache it?\n // For now, let's keep it simple: createLogger per request.\n // If performance becomes an issue, we can refactor `createLogger` to reuse the winston instance if options match.\n\n const rootLogger = createLogger(options);\n\n // 2. Extract Context\n const requestContext: Record<string, any> = {};\n if (context) {\n if (context.awsRequestId) requestContext.requestId = context.awsRequestId;\n if (context.functionName) requestContext.functionName = context.functionName;\n }\n\n // 3. Create Child Logger with Request Context\n const scopedLogger = rootLogger.child(requestContext);\n\n // 3a. Log Event and Context\n scopedLogger.debug('Lambda Event', { event });\n scopedLogger.debug('Lambda Context', { context });\n\n // 4. Run Handler in Context\n return runWithLogger(\n scopedLogger,\n async () => {\n // We use 'await' to ensure the context stays active during the handler execution\n // If the handler accepts a callback, we might need special handling?\n // Most modern lambdas use async/await.\n // If legacy callback style is used, AsyncLocalStorage context *should* still propagate\n // if the callback is invoked asynchronously.\n // But we wrap the result in Promise usually.\n\n // Supporting both async and callback style:\n try {\n // If it returns a promise, await it\n const result = await handler(event, context, callback);\n return result as TResult;\n } catch (error) {\n // We could log unhandled errors here too?\n // Standard lambda practice is to let the error propagate so Lambda runtime sees it (and retries etc)\n // BUT we should log it first because once it leaves here, we might lose the logger context behavior.\n scopedLogger.error('Unhandled Lambda Exception', error as Error);\n throw error;\n }\n },\n requestContext.requestId,\n );\n };\n};\n"],"mappings":"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","names":["crypto","message","winston"]}
|
package/dist/index.mjs
CHANGED
|
@@ -94,13 +94,17 @@ var createRedactor = (config) => {
|
|
|
94
94
|
// src/context.ts
|
|
95
95
|
import { AsyncLocalStorage } from "async_hooks";
|
|
96
96
|
var asyncLocalStorage = new AsyncLocalStorage();
|
|
97
|
-
var runWithLogger = (logger, callback) => {
|
|
98
|
-
return asyncLocalStorage.run({ logger }, callback);
|
|
97
|
+
var runWithLogger = (logger, callback, requestId) => {
|
|
98
|
+
return asyncLocalStorage.run({ logger, requestId }, callback);
|
|
99
99
|
};
|
|
100
100
|
var getLogger = () => {
|
|
101
101
|
const store = asyncLocalStorage.getStore();
|
|
102
102
|
return store?.logger;
|
|
103
103
|
};
|
|
104
|
+
var getRequestId = () => {
|
|
105
|
+
const store = asyncLocalStorage.getStore();
|
|
106
|
+
return store?.requestId;
|
|
107
|
+
};
|
|
104
108
|
var updateLoggerContext = (newLogger) => {
|
|
105
109
|
const store = asyncLocalStorage.getStore();
|
|
106
110
|
if (store) {
|
|
@@ -134,18 +138,32 @@ var createLoggerWrapper = (winstonLogger) => {
|
|
|
134
138
|
warn: (message, context) => {
|
|
135
139
|
winstonLogger.warn(message, formatContext(context));
|
|
136
140
|
},
|
|
137
|
-
error: (message,
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
...rest
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
error: (message, errorOrContext, context) => {
|
|
142
|
+
let meta = formatContext(context);
|
|
143
|
+
if (errorOrContext instanceof Error || typeof errorOrContext === "string") {
|
|
144
|
+
const error = errorOrContext;
|
|
145
|
+
if (error instanceof Error) {
|
|
146
|
+
const { message: message2, name, stack, ...rest } = error;
|
|
147
|
+
meta.error = {
|
|
148
|
+
...rest,
|
|
149
|
+
message: message2,
|
|
150
|
+
name,
|
|
151
|
+
stack
|
|
152
|
+
};
|
|
153
|
+
} else {
|
|
154
|
+
meta.error = error;
|
|
155
|
+
}
|
|
156
|
+
} else if (typeof errorOrContext === "object" && errorOrContext !== null) {
|
|
157
|
+
meta = { ...meta, ...errorOrContext };
|
|
158
|
+
if (meta.error instanceof Error) {
|
|
159
|
+
const { message: message2, name, stack, ...rest } = meta.error;
|
|
160
|
+
meta.error = {
|
|
161
|
+
...rest,
|
|
162
|
+
message: message2,
|
|
163
|
+
name,
|
|
164
|
+
stack
|
|
165
|
+
};
|
|
166
|
+
}
|
|
149
167
|
}
|
|
150
168
|
winstonLogger.error(message, meta);
|
|
151
169
|
},
|
|
@@ -190,15 +208,19 @@ var withLogger = (handler, options = {}) => {
|
|
|
190
208
|
const scopedLogger = rootLogger.child(requestContext);
|
|
191
209
|
scopedLogger.debug("Lambda Event", { event });
|
|
192
210
|
scopedLogger.debug("Lambda Context", { context });
|
|
193
|
-
return runWithLogger(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
return runWithLogger(
|
|
212
|
+
scopedLogger,
|
|
213
|
+
async () => {
|
|
214
|
+
try {
|
|
215
|
+
const result = await handler(event, context, callback);
|
|
216
|
+
return result;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
scopedLogger.error("Unhandled Lambda Exception", error);
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
requestContext.requestId
|
|
223
|
+
);
|
|
202
224
|
};
|
|
203
225
|
};
|
|
204
226
|
export {
|
|
@@ -208,6 +230,7 @@ export {
|
|
|
208
230
|
createLogger,
|
|
209
231
|
createRedactor,
|
|
210
232
|
getLogger,
|
|
233
|
+
getRequestId,
|
|
211
234
|
recursiveRedact,
|
|
212
235
|
runWithLogger,
|
|
213
236
|
updateLoggerContext,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/redactor.ts","../src/context.ts","../src/logger.ts","../src/constants.ts","../src/wrapper.ts"],"sourcesContent":["import crypto from 'crypto';\n\nexport type RedactionStrategy = 'mask' | 'remove' | 'hash' | string | ((value: any) => any);\n\nexport interface RedactionConfig {\n /**\n * Array of keys to redact.\n * Case-insensitive matching is recommended/implemented.\n */\n keys: string[];\n /**\n * Map of specific key to strategy.\n * If a key is in 'keys' but not here, it uses the default strategy ('mask').\n */\n strategies?: Record<string, RedactionStrategy>;\n /**\n * Default strategy for keys found in the list but not in the strategy map.\n * Default: 'mask'\n */\n defaultStrategy?: RedactionStrategy;\n}\n\nexport const COMMON_REDACTION_KEYS = [\n 'password',\n 'passwd',\n 'secret',\n 'token',\n 'access_token',\n 'access-token',\n 'auth',\n 'auth-token',\n 'authorization',\n 'apikey',\n 'api_key',\n 'api-key',\n 'card',\n 'cvv',\n 'ssn',\n 'pin',\n];\n\n/**\n * Applies the redaction strategy to a single value.\n */\nconst applyStrategy = (value: any, strategy: RedactionStrategy): any => {\n if (typeof strategy === 'function') {\n return strategy(value);\n }\n\n if (typeof strategy === 'string' && strategy.startsWith('mask-last-')) {\n const parts = strategy.split('-');\n const lastN = parseInt(parts[2], 10);\n\n if (typeof value === 'string' && !isNaN(lastN)) {\n if (value.length <= lastN) {\n return value; // Or mask entirely? Usually if shorter, we show it or mask all. Let's return as is or mask all?\n // Security wise: showing short secrets fully is bad. Masking all is safer.\n // BUT, if user asked for last 4 and length is 3, showing 3 is effectively \"last 4\".\n // Let's mask all if length <= lastN to be safe? Or just return.\n // Actually, standard is usually: if len <= N, show all (it IS the last N).\n // But for API keys, usually we want to see SOMETHING masked.\n // Let's preserve standard behavior: visible suffix.\n return value;\n }\n const maskLen = value.length - lastN;\n return '*'.repeat(maskLen) + value.slice(-lastN);\n }\n return '*****'; // Fallback for non-strings\n }\n\n switch (strategy) {\n case 'remove':\n return undefined;\n case 'hash':\n if (typeof value === 'string' || typeof value === 'number') {\n return crypto.createHash('sha256').update(String(value)).digest('hex');\n }\n return '[HASH_FAILED_TYPE]';\n case 'mask':\n default:\n return '*****';\n }\n};\n\n/**\n * Recursively walks the object and redacts fields matching the config.\n */\nexport const recursiveRedact = (\n target: any,\n config: RedactionConfig,\n cache = new Set<any>(),\n): any => {\n if (target === null || typeof target !== 'object') {\n return target;\n }\n\n // Handle circular references\n if (cache.has(target)) {\n return '[Circular]';\n }\n cache.add(target);\n\n // Handle Arrays\n if (Array.isArray(target)) {\n return target.map((item) => recursiveRedact(item, config, cache));\n }\n\n // Handle Objects\n const redactedObj: Record<string, any> = {};\n const keysToRedact = new Set(config.keys.map((k) => k.toLowerCase()));\n\n for (const [key, value] of Object.entries(target)) {\n const lowerKey = key.toLowerCase();\n\n if (keysToRedact.has(lowerKey)) {\n // Determine strategy\n let strategy = config.defaultStrategy || 'mask';\n if (config.strategies && config.strategies[key]) {\n // Try exact match first\n strategy = config.strategies[key];\n } else if (config.strategies) {\n // Try case-insensitive lookup in strategies if needed,\n // but for simplicity let's rely on specific keys matching or default.\n // Actually, user might put 'Password': 'hash' and we matched 'password'.\n // Let's iterate strategies keys to find case-insensitive match if strictly needed.\n // For performance/simplicity, strict case matching in strategies map is safer,\n // but generic 'keys' list is case-insensitive.\n\n // Let's look for a key in strategies that matches lowerKey\n const strategyKey = Object.keys(config.strategies).find(\n (k) => k.toLowerCase() === lowerKey,\n );\n if (strategyKey) {\n strategy = config.strategies[strategyKey];\n }\n }\n\n const result = applyStrategy(value, strategy);\n if (result !== undefined) {\n redactedObj[key] = result;\n }\n // If undefined (remove strategy), we just don't add the key.\n } else {\n // Recurse\n redactedObj[key] = recursiveRedact(value, config, cache);\n }\n }\n\n return redactedObj;\n};\n\nexport const createRedactor = (config: RedactionConfig) => {\n return (info: any) => {\n return recursiveRedact(info, config);\n };\n};\n","import { AsyncLocalStorage } from 'async_hooks';\nimport { Logger } from './types';\n\n// We store a reference to the logger so we can update it in-place for the current context\ninterface LoggerStore {\n logger: Logger;\n}\n\nconst asyncLocalStorage = new AsyncLocalStorage<LoggerStore>();\n\n/**\n * Runs a callback within a logger context.\n * The logger provided becomes the \"current\" logger for the duration of the callback.\n */\nexport const runWithLogger = <T>(logger: Logger, callback: () => T): T => {\n return asyncLocalStorage.run({ logger }, callback);\n};\n\n/**\n * Gets the current logger from the async context.\n * Throws an error if called outside of a runWithLogger context,\n * unless a fallback is provided (though usually we want to enforce context).\n *\n * To make it easier to use, we can return undefined or a default logger if needed,\n * but for this specific feature request \"access by all down the stream\", strictness is good.\n */\nexport const getLogger = (): Logger | undefined => {\n const store = asyncLocalStorage.getStore();\n return store?.logger;\n};\n\n/**\n * Updates the current context's logger.\n * This effectively \"extends\" the logger for the remainder of the current async execution\n * and any further downstream calls sharing this context.\n */\nexport const updateLoggerContext = (newLogger: Logger): void => {\n const store = asyncLocalStorage.getStore();\n if (store) {\n store.logger = newLogger;\n } else {\n // If we are not in a context, we can't update it.\n // We could throw, or warn. For now, let's warn.\n console.warn(\n 'AntigravityLogger: updateLoggerContext called outside of an active context. Logic ignored.',\n );\n }\n};\n","import winston from 'winston';\nimport { Logger } from './types';\nimport { DEFAULT_LOG_LEVEL } from './constants';\nimport { RedactionConfig, createRedactor } from './redactor';\n\nexport interface CreateLoggerOptions {\n /**\n * Default context to include in all logs\n */\n defaultContext?: Record<string, any>;\n /**\n * Log level (default: process.env.LOG_LEVEL || 'info')\n */\n level?: string;\n /**\n * Configuration for data redaction\n */\n redaction?: RedactionConfig;\n}\n\nconst formatContext = (context?: Record<string, any>): Record<string, any> => {\n return context || {};\n};\n\nconst createLoggerWrapper = (winstonLogger: winston.Logger): Logger => {\n return {\n debug: (message: string, context?: Record<string, any>): void => {\n winstonLogger.debug(message, formatContext(context));\n },\n\n info: (message: string, context?: Record<string, any>): void => {\n winstonLogger.info(message, formatContext(context));\n },\n\n warn: (message: string, context?: Record<string, any>): void => {\n winstonLogger.warn(message, formatContext(context));\n },\n\n error: (message: string, error?: Error | string, context?: Record<string, any>): void => {\n const meta = formatContext(context);\n if (error instanceof Error) {\n // Winston handles error objects well if passed as meta or splat\n // But for explicit structure, let's attach it\n // Spread error first to capture custom props, then overwrite standard ones to ensure presence\n const { message, name, stack, ...rest } = error;\n meta.error = {\n ...rest,\n message,\n name,\n stack,\n };\n } else if (error) {\n meta.error = error;\n }\n winstonLogger.error(message, meta);\n },\n\n child: (context: Record<string, any>): Logger => {\n // Winston's child() returns a new logger instance with the metadata bound\n return createLoggerWrapper(winstonLogger.child(context));\n },\n };\n};\n\nexport const createLogger = (options: CreateLoggerOptions = {}): Logger => {\n const level = options.level || process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL;\n\n // Create redactor if config is present\n const redactor = options.redaction ? createRedactor(options.redaction) : null;\n\n // Custom format that applies redaction\n const redactionFormat = winston.format((info) => {\n if (redactor) {\n return redactor(info);\n }\n return info;\n });\n\n const format = winston.format.combine(\n winston.format.timestamp(),\n redactionFormat(), // Apply redaction before JSON\n winston.format.json(),\n );\n\n const winstonLogger = winston.createLogger({\n level,\n format,\n defaultMeta: options.defaultContext,\n transports: [new winston.transports.Console()],\n });\n\n return createLoggerWrapper(winstonLogger);\n};\n","export const DEFAULT_LOG_LEVEL = 'info';\nexport const REQUEST_ID_KEY = 'x-request-id';\n","import { CreateLoggerOptions, createLogger } from './logger';\nimport { runWithLogger } from './context';\n\n// Generic Handler type compatible with AWS Lambda\n// We use 'any' to avoid strict dependency on @types/aws-lambda for this generic wrapper\n// but in practice it wraps (event, context, callback?) => Promise<any> | any\ntype Handler<TEvent = any, TResult = any> = (\n event: TEvent,\n context: any,\n callback?: any,\n) => Promise<TResult> | void;\n\n/**\n * Higher-order function to wrap a Lambda handler with logger context.\n * Automatically extracts 'awsRequestId' and 'functionName' from the Lambda context\n * and initializes a logger for the request scope.\n *\n * @param handler The original Lambda handler\n * @param options Logger options (level, redaction, etc.)\n */\nexport const withLogger = <TEvent = any, TResult = any>(\n handler: Handler<TEvent, TResult>,\n options: CreateLoggerOptions = {},\n): Handler<TEvent, TResult> => {\n return async (event: TEvent, context: any, callback?: any) => {\n // 1. Initialize Logger\n // Create a root logger if custom options are provided, or use default behavior.\n // We create a FRESH logger instance for each request to ensure context isolation if needed?\n // Actually, creating a logger instance is cheap?\n // Winston createLogger is relatively heavy.\n // Optimization: We could reuse a global logger and just child() it.\n // But options might change? Usually options are static.\n // Let's assume options are static per lambda wrapper usage.\n\n // HOWEVER, to support dynamic options per request might be overkill.\n // Let's create one global logger instance for this wrapper instantiation if possible?\n // But wait, the standard `createLogger` we implemented creates a NEW winston instance every time.\n // We should probably cache it?\n // For now, let's keep it simple: createLogger per request.\n // If performance becomes an issue, we can refactor `createLogger` to reuse the winston instance if options match.\n\n const rootLogger = createLogger(options);\n\n // 2. Extract Context\n const requestContext: Record<string, any> = {};\n if (context) {\n if (context.awsRequestId) requestContext.requestId = context.awsRequestId;\n if (context.functionName) requestContext.functionName = context.functionName;\n }\n\n // 3. Create Child Logger with Request Context\n const scopedLogger = rootLogger.child(requestContext);\n\n // 3a. Log Event and Context\n scopedLogger.debug('Lambda Event', { event });\n scopedLogger.debug('Lambda Context', { context });\n\n // 4. Run Handler in Context\n return runWithLogger(scopedLogger, async () => {\n // We use 'await' to ensure the context stays active during the handler execution\n // If the handler accepts a callback, we might need special handling?\n // Most modern lambdas use async/await.\n // If legacy callback style is used, AsyncLocalStorage context *should* still propagate\n // if the callback is invoked asynchronously.\n // But we wrap the result in Promise usually.\n\n // Supporting both async and callback style:\n try {\n // If it returns a promise, await it\n const result = await handler(event, context, callback);\n return result as TResult;\n } catch (error) {\n // We could log unhandled errors here too?\n // Standard lambda practice is to let the error propagate so Lambda runtime sees it (and retries etc)\n // BUT we should log it first because once it leaves here, we might lose the logger context behavior.\n scopedLogger.error('Unhandled Lambda Exception', error as Error);\n throw error;\n }\n });\n };\n};\n"],"mappings":"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","names":["message"]}
|
|
1
|
+
{"version":3,"sources":["../src/redactor.ts","../src/context.ts","../src/logger.ts","../src/constants.ts","../src/wrapper.ts"],"sourcesContent":["import crypto from 'crypto';\n\nexport type RedactionStrategy = 'mask' | 'remove' | 'hash' | string | ((value: any) => any);\n\nexport interface RedactionConfig {\n /**\n * Array of keys to redact.\n * Case-insensitive matching is recommended/implemented.\n */\n keys: string[];\n /**\n * Map of specific key to strategy.\n * If a key is in 'keys' but not here, it uses the default strategy ('mask').\n */\n strategies?: Record<string, RedactionStrategy>;\n /**\n * Default strategy for keys found in the list but not in the strategy map.\n * Default: 'mask'\n */\n defaultStrategy?: RedactionStrategy;\n}\n\nexport const COMMON_REDACTION_KEYS = [\n 'password',\n 'passwd',\n 'secret',\n 'token',\n 'access_token',\n 'access-token',\n 'auth',\n 'auth-token',\n 'authorization',\n 'apikey',\n 'api_key',\n 'api-key',\n 'card',\n 'cvv',\n 'ssn',\n 'pin',\n];\n\n/**\n * Applies the redaction strategy to a single value.\n */\nconst applyStrategy = (value: any, strategy: RedactionStrategy): any => {\n if (typeof strategy === 'function') {\n return strategy(value);\n }\n\n if (typeof strategy === 'string' && strategy.startsWith('mask-last-')) {\n const parts = strategy.split('-');\n const lastN = parseInt(parts[2], 10);\n\n if (typeof value === 'string' && !isNaN(lastN)) {\n if (value.length <= lastN) {\n return value; // Or mask entirely? Usually if shorter, we show it or mask all. Let's return as is or mask all?\n // Security wise: showing short secrets fully is bad. Masking all is safer.\n // BUT, if user asked for last 4 and length is 3, showing 3 is effectively \"last 4\".\n // Let's mask all if length <= lastN to be safe? Or just return.\n // Actually, standard is usually: if len <= N, show all (it IS the last N).\n // But for API keys, usually we want to see SOMETHING masked.\n // Let's preserve standard behavior: visible suffix.\n return value;\n }\n const maskLen = value.length - lastN;\n return '*'.repeat(maskLen) + value.slice(-lastN);\n }\n return '*****'; // Fallback for non-strings\n }\n\n switch (strategy) {\n case 'remove':\n return undefined;\n case 'hash':\n if (typeof value === 'string' || typeof value === 'number') {\n return crypto.createHash('sha256').update(String(value)).digest('hex');\n }\n return '[HASH_FAILED_TYPE]';\n case 'mask':\n default:\n return '*****';\n }\n};\n\n/**\n * Recursively walks the object and redacts fields matching the config.\n */\nexport const recursiveRedact = (\n target: any,\n config: RedactionConfig,\n cache = new Set<any>(),\n): any => {\n if (target === null || typeof target !== 'object') {\n return target;\n }\n\n // Handle circular references\n if (cache.has(target)) {\n return '[Circular]';\n }\n cache.add(target);\n\n // Handle Arrays\n if (Array.isArray(target)) {\n return target.map((item) => recursiveRedact(item, config, cache));\n }\n\n // Handle Objects\n const redactedObj: Record<string, any> = {};\n const keysToRedact = new Set(config.keys.map((k) => k.toLowerCase()));\n\n for (const [key, value] of Object.entries(target)) {\n const lowerKey = key.toLowerCase();\n\n if (keysToRedact.has(lowerKey)) {\n // Determine strategy\n let strategy = config.defaultStrategy || 'mask';\n if (config.strategies && config.strategies[key]) {\n // Try exact match first\n strategy = config.strategies[key];\n } else if (config.strategies) {\n // Try case-insensitive lookup in strategies if needed,\n // but for simplicity let's rely on specific keys matching or default.\n // Actually, user might put 'Password': 'hash' and we matched 'password'.\n // Let's iterate strategies keys to find case-insensitive match if strictly needed.\n // For performance/simplicity, strict case matching in strategies map is safer,\n // but generic 'keys' list is case-insensitive.\n\n // Let's look for a key in strategies that matches lowerKey\n const strategyKey = Object.keys(config.strategies).find(\n (k) => k.toLowerCase() === lowerKey,\n );\n if (strategyKey) {\n strategy = config.strategies[strategyKey];\n }\n }\n\n const result = applyStrategy(value, strategy);\n if (result !== undefined) {\n redactedObj[key] = result;\n }\n // If undefined (remove strategy), we just don't add the key.\n } else {\n // Recurse\n redactedObj[key] = recursiveRedact(value, config, cache);\n }\n }\n\n return redactedObj;\n};\n\nexport const createRedactor = (config: RedactionConfig) => {\n return (info: any) => {\n return recursiveRedact(info, config);\n };\n};\n","import { AsyncLocalStorage } from 'async_hooks';\nimport { Logger } from './types';\n\n// We store a reference to the logger so we can update it in-place for the current context\ninterface LoggerStore {\n logger: Logger;\n requestId?: string;\n}\n\nconst asyncLocalStorage = new AsyncLocalStorage<LoggerStore>();\n\n/**\n * Runs a callback within a logger context.\n * The logger provided becomes the \"current\" logger for the duration of the callback.\n */\nexport const runWithLogger = <T>(logger: Logger, callback: () => T, requestId?: string): T => {\n return asyncLocalStorage.run({ logger, requestId }, callback);\n};\n\n/**\n * Gets the current logger from the async context.\n */\nexport const getLogger = (): Logger | undefined => {\n const store = asyncLocalStorage.getStore();\n return store?.logger;\n};\n\n/**\n * Gets the current request ID from the async context.\n */\nexport const getRequestId = (): string | undefined => {\n const store = asyncLocalStorage.getStore();\n return store?.requestId;\n};\n\n/**\n * Updates the current context's logger.\n * This effectively \"extends\" the logger for the remainder of the current async execution\n * and any further downstream calls sharing this context.\n */\nexport const updateLoggerContext = (newLogger: Logger): void => {\n const store = asyncLocalStorage.getStore();\n if (store) {\n store.logger = newLogger;\n } else {\n // If we are not in a context, we can't update it.\n // We could throw, or warn. For now, let's warn.\n console.warn(\n 'AntigravityLogger: updateLoggerContext called outside of an active context. Logic ignored.',\n );\n }\n};\n","import winston from 'winston';\nimport { Logger } from './types';\nimport { DEFAULT_LOG_LEVEL } from './constants';\nimport { RedactionConfig, createRedactor } from './redactor';\n\nexport interface CreateLoggerOptions {\n /**\n * Default context to include in all logs\n */\n defaultContext?: Record<string, any>;\n /**\n * Log level (default: process.env.LOG_LEVEL || 'info')\n */\n level?: string;\n /**\n * Configuration for data redaction\n */\n redaction?: RedactionConfig;\n}\n\nconst formatContext = (context?: Record<string, any>): Record<string, any> => {\n return context || {};\n};\n\nconst createLoggerWrapper = (winstonLogger: winston.Logger): Logger => {\n return {\n debug: (message: string, context?: Record<string, any>): void => {\n winstonLogger.debug(message, formatContext(context));\n },\n\n info: (message: string, context?: Record<string, any>): void => {\n winstonLogger.info(message, formatContext(context));\n },\n\n warn: (message: string, context?: Record<string, any>): void => {\n winstonLogger.warn(message, formatContext(context));\n },\n\n error: (\n message: string,\n errorOrContext?: Error | string | Record<string, any>,\n context?: Record<string, any>,\n ): void => {\n let meta = formatContext(context);\n\n if (errorOrContext instanceof Error || typeof errorOrContext === 'string') {\n // Old behavior: second arg is the error\n const error = errorOrContext;\n if (error instanceof Error) {\n const { message, name, stack, ...rest } = error;\n meta.error = {\n ...rest,\n message,\n name,\n stack,\n };\n } else {\n meta.error = error;\n }\n } else if (typeof errorOrContext === 'object' && errorOrContext !== null) {\n // New behavior: second arg is context (which might contain error)\n meta = { ...meta, ...errorOrContext };\n\n // If error is in the context, format it nicely\n if (meta.error instanceof Error) {\n const { message, name, stack, ...rest } = meta.error;\n meta.error = {\n ...rest,\n message,\n name,\n stack,\n };\n }\n }\n\n winstonLogger.error(message, meta);\n },\n\n child: (context: Record<string, any>): Logger => {\n // Winston's child() returns a new logger instance with the metadata bound\n return createLoggerWrapper(winstonLogger.child(context));\n },\n };\n};\n\nexport const createLogger = (options: CreateLoggerOptions = {}): Logger => {\n const level = options.level || process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL;\n\n // Create redactor if config is present\n const redactor = options.redaction ? createRedactor(options.redaction) : null;\n\n // Custom format that applies redaction\n const redactionFormat = winston.format((info) => {\n if (redactor) {\n return redactor(info);\n }\n return info;\n });\n\n const format = winston.format.combine(\n winston.format.timestamp(),\n redactionFormat(), // Apply redaction before JSON\n winston.format.json(),\n );\n\n const winstonLogger = winston.createLogger({\n level,\n format,\n defaultMeta: options.defaultContext,\n transports: [new winston.transports.Console()],\n });\n\n return createLoggerWrapper(winstonLogger);\n};\n","export const DEFAULT_LOG_LEVEL = 'info';\nexport const REQUEST_ID_KEY = 'x-request-id';\n","import { CreateLoggerOptions, createLogger } from './logger';\nimport { runWithLogger } from './context';\n\n// Generic Handler type compatible with AWS Lambda\n// We use 'any' to avoid strict dependency on @types/aws-lambda for this generic wrapper\n// but in practice it wraps (event, context, callback?) => Promise<any> | any\ntype Handler<TEvent = any, TResult = any> = (\n event: TEvent,\n context: any,\n callback?: any,\n) => Promise<TResult> | void;\n\n/**\n * Higher-order function to wrap a Lambda handler with logger context.\n * Automatically extracts 'awsRequestId' and 'functionName' from the Lambda context\n * and initializes a logger for the request scope.\n *\n * @param handler The original Lambda handler\n * @param options Logger options (level, redaction, etc.)\n */\nexport const withLogger = <TEvent = any, TResult = any>(\n handler: Handler<TEvent, TResult>,\n options: CreateLoggerOptions = {},\n): Handler<TEvent, TResult> => {\n return async (event: TEvent, context: any, callback?: any) => {\n // 1. Initialize Logger\n // Create a root logger if custom options are provided, or use default behavior.\n // We create a FRESH logger instance for each request to ensure context isolation if needed?\n // Actually, creating a logger instance is cheap?\n // Winston createLogger is relatively heavy.\n // Optimization: We could reuse a global logger and just child() it.\n // But options might change? Usually options are static.\n // Let's assume options are static per lambda wrapper usage.\n\n // HOWEVER, to support dynamic options per request might be overkill.\n // Let's create one global logger instance for this wrapper instantiation if possible?\n // But wait, the standard `createLogger` we implemented creates a NEW winston instance every time.\n // We should probably cache it?\n // For now, let's keep it simple: createLogger per request.\n // If performance becomes an issue, we can refactor `createLogger` to reuse the winston instance if options match.\n\n const rootLogger = createLogger(options);\n\n // 2. Extract Context\n const requestContext: Record<string, any> = {};\n if (context) {\n if (context.awsRequestId) requestContext.requestId = context.awsRequestId;\n if (context.functionName) requestContext.functionName = context.functionName;\n }\n\n // 3. Create Child Logger with Request Context\n const scopedLogger = rootLogger.child(requestContext);\n\n // 3a. Log Event and Context\n scopedLogger.debug('Lambda Event', { event });\n scopedLogger.debug('Lambda Context', { context });\n\n // 4. Run Handler in Context\n return runWithLogger(\n scopedLogger,\n async () => {\n // We use 'await' to ensure the context stays active during the handler execution\n // If the handler accepts a callback, we might need special handling?\n // Most modern lambdas use async/await.\n // If legacy callback style is used, AsyncLocalStorage context *should* still propagate\n // if the callback is invoked asynchronously.\n // But we wrap the result in Promise usually.\n\n // Supporting both async and callback style:\n try {\n // If it returns a promise, await it\n const result = await handler(event, context, callback);\n return result as TResult;\n } catch (error) {\n // We could log unhandled errors here too?\n // Standard lambda practice is to let the error propagate so Lambda runtime sees it (and retries etc)\n // BUT we should log it first because once it leaves here, we might lose the logger context behavior.\n scopedLogger.error('Unhandled Lambda Exception', error as Error);\n throw error;\n }\n },\n requestContext.requestId,\n );\n };\n};\n"],"mappings":"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","names":["message"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitkuz/aws-logger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Standardized AWS Logger using Winston",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
"test:redaction": "tsx test/redaction-test.ts",
|
|
15
15
|
"test:async-context": "tsx test/async-context.test.ts",
|
|
16
16
|
"test:wrapper": "tsx test/wrapper.test.ts",
|
|
17
|
-
"format": "prettier --write
|
|
18
|
-
"prepublishOnly": "npm run format && npm run build"
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"prepublishOnly": "npm run format && npm run build",
|
|
19
|
+
"check-format": "prettier --check ."
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"winston": "^3.11.0"
|
|
@@ -26,5 +27,14 @@
|
|
|
26
27
|
"tsup": "^8.0.1",
|
|
27
28
|
"tsx": "^4.7.0",
|
|
28
29
|
"typescript": "^5.3.3"
|
|
29
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"author": "Vitali Kuzmenka",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/vitkuz/vitkuz-aws-logger.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/vitkuz/vitkuz-aws-logger/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/vitkuz/vitkuz-aws-logger#readme"
|
|
30
40
|
}
|
package/src/context.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Logger } from './types';
|
|
|
4
4
|
// We store a reference to the logger so we can update it in-place for the current context
|
|
5
5
|
interface LoggerStore {
|
|
6
6
|
logger: Logger;
|
|
7
|
+
requestId?: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const asyncLocalStorage = new AsyncLocalStorage<LoggerStore>();
|
|
@@ -12,23 +13,26 @@ const asyncLocalStorage = new AsyncLocalStorage<LoggerStore>();
|
|
|
12
13
|
* Runs a callback within a logger context.
|
|
13
14
|
* The logger provided becomes the "current" logger for the duration of the callback.
|
|
14
15
|
*/
|
|
15
|
-
export const runWithLogger = <T>(logger: Logger, callback: () => T): T => {
|
|
16
|
-
return asyncLocalStorage.run({ logger }, callback);
|
|
16
|
+
export const runWithLogger = <T>(logger: Logger, callback: () => T, requestId?: string): T => {
|
|
17
|
+
return asyncLocalStorage.run({ logger, requestId }, callback);
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Gets the current logger from the async context.
|
|
21
|
-
* Throws an error if called outside of a runWithLogger context,
|
|
22
|
-
* unless a fallback is provided (though usually we want to enforce context).
|
|
23
|
-
*
|
|
24
|
-
* To make it easier to use, we can return undefined or a default logger if needed,
|
|
25
|
-
* but for this specific feature request "access by all down the stream", strictness is good.
|
|
26
22
|
*/
|
|
27
23
|
export const getLogger = (): Logger | undefined => {
|
|
28
24
|
const store = asyncLocalStorage.getStore();
|
|
29
25
|
return store?.logger;
|
|
30
26
|
};
|
|
31
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Gets the current request ID from the async context.
|
|
30
|
+
*/
|
|
31
|
+
export const getRequestId = (): string | undefined => {
|
|
32
|
+
const store = asyncLocalStorage.getStore();
|
|
33
|
+
return store?.requestId;
|
|
34
|
+
};
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Updates the current context's logger.
|
|
34
38
|
* This effectively "extends" the logger for the remainder of the current async execution
|
package/src/logger.ts
CHANGED
|
@@ -36,22 +36,43 @@ const createLoggerWrapper = (winstonLogger: winston.Logger): Logger => {
|
|
|
36
36
|
winstonLogger.warn(message, formatContext(context));
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
-
error: (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
stack,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
error: (
|
|
40
|
+
message: string,
|
|
41
|
+
errorOrContext?: Error | string | Record<string, any>,
|
|
42
|
+
context?: Record<string, any>,
|
|
43
|
+
): void => {
|
|
44
|
+
let meta = formatContext(context);
|
|
45
|
+
|
|
46
|
+
if (errorOrContext instanceof Error || typeof errorOrContext === 'string') {
|
|
47
|
+
// Old behavior: second arg is the error
|
|
48
|
+
const error = errorOrContext;
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
const { message, name, stack, ...rest } = error;
|
|
51
|
+
meta.error = {
|
|
52
|
+
...rest,
|
|
53
|
+
message,
|
|
54
|
+
name,
|
|
55
|
+
stack,
|
|
56
|
+
};
|
|
57
|
+
} else {
|
|
58
|
+
meta.error = error;
|
|
59
|
+
}
|
|
60
|
+
} else if (typeof errorOrContext === 'object' && errorOrContext !== null) {
|
|
61
|
+
// New behavior: second arg is context (which might contain error)
|
|
62
|
+
meta = { ...meta, ...errorOrContext };
|
|
63
|
+
|
|
64
|
+
// If error is in the context, format it nicely
|
|
65
|
+
if (meta.error instanceof Error) {
|
|
66
|
+
const { message, name, stack, ...rest } = meta.error;
|
|
67
|
+
meta.error = {
|
|
68
|
+
...rest,
|
|
69
|
+
message,
|
|
70
|
+
name,
|
|
71
|
+
stack,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
54
74
|
}
|
|
75
|
+
|
|
55
76
|
winstonLogger.error(message, meta);
|
|
56
77
|
},
|
|
57
78
|
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,11 @@ export interface Logger {
|
|
|
2
2
|
debug(message: string, context?: Record<string, any>): void;
|
|
3
3
|
info(message: string, context?: Record<string, any>): void;
|
|
4
4
|
warn(message: string, context?: Record<string, any>): void;
|
|
5
|
-
error(
|
|
5
|
+
error(
|
|
6
|
+
message: string,
|
|
7
|
+
errorOrContext?: Error | string | Record<string, any>,
|
|
8
|
+
context?: Record<string, any>,
|
|
9
|
+
): void;
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Creates a child logger with bound context.
|
package/src/wrapper.ts
CHANGED
|
@@ -56,26 +56,30 @@ export const withLogger = <TEvent = any, TResult = any>(
|
|
|
56
56
|
scopedLogger.debug('Lambda Context', { context });
|
|
57
57
|
|
|
58
58
|
// 4. Run Handler in Context
|
|
59
|
-
return runWithLogger(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
return runWithLogger(
|
|
60
|
+
scopedLogger,
|
|
61
|
+
async () => {
|
|
62
|
+
// We use 'await' to ensure the context stays active during the handler execution
|
|
63
|
+
// If the handler accepts a callback, we might need special handling?
|
|
64
|
+
// Most modern lambdas use async/await.
|
|
65
|
+
// If legacy callback style is used, AsyncLocalStorage context *should* still propagate
|
|
66
|
+
// if the callback is invoked asynchronously.
|
|
67
|
+
// But we wrap the result in Promise usually.
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
// Supporting both async and callback style:
|
|
70
|
+
try {
|
|
71
|
+
// If it returns a promise, await it
|
|
72
|
+
const result = await handler(event, context, callback);
|
|
73
|
+
return result as TResult;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// We could log unhandled errors here too?
|
|
76
|
+
// Standard lambda practice is to let the error propagate so Lambda runtime sees it (and retries etc)
|
|
77
|
+
// BUT we should log it first because once it leaves here, we might lose the logger context behavior.
|
|
78
|
+
scopedLogger.error('Unhandled Lambda Exception', error as Error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
requestContext.requestId,
|
|
83
|
+
);
|
|
80
84
|
};
|
|
81
85
|
};
|