voltlog-io 1.0.5 → 1.0.7

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.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/middleware/async-context.ts","../src/middleware/correlation-id.ts","../src/transports/file.ts","../src/transports/json-stream.ts","../src/transports/redis.ts"],"sourcesContent":["/**\n * @module voltlog-io\n * @description AsyncLocalStorage context middleware — automatically propagates\n * context across async boundaries without manual child logger threading.\n *\n * @server-only Requires Node.js 16+ (AsyncLocalStorage).\n *\n * @example\n * ```ts\n * import { createLogger, prettyTransport } from 'voltlog-io';\n * import { asyncContextMiddleware } from 'voltlog-io';\n *\n * const asyncCtx = asyncContextMiddleware();\n *\n * const logger = createLogger({\n * middleware: [asyncCtx.middleware],\n * transports: [prettyTransport()],\n * });\n *\n * // In Express middleware — set context once\n * app.use((req, res, next) => {\n * asyncCtx.runInContext({ requestId: req.id, userId: req.user?.id }, next);\n * });\n *\n * // Anywhere downstream — context is automatic\n * async function processOrder(orderId: string) {\n * logger.info('Processing', { orderId });\n * // → auto-includes: { requestId: 'req-123', userId: 'u-42', orderId: '...' }\n *\n * await chargePayment(orderId);\n * logger.info('Payment charged');\n * // → still has requestId and userId, even after await!\n * }\n * ```\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { LogMiddleware } from \"../core/types.js\";\n\nexport interface AsyncContextResult<TMeta = Record<string, unknown>> {\n /** Middleware to add to your logger — injects async context into entries */\n middleware: LogMiddleware<TMeta>;\n\n /**\n * Run a function with the given context.\n * All logs within the function (and any async calls it makes) will\n * automatically include this context in their metadata.\n */\n runInContext: (context: Record<string, unknown>, fn: () => void) => void;\n\n /**\n * Get the current async context, or undefined if not inside a runInContext call.\n */\n getContext: () => Record<string, unknown> | undefined;\n\n /**\n * Update the current async context by merging new values.\n * Only works inside a runInContext call.\n */\n updateContext: (updates: Record<string, unknown>) => void;\n}\n\n/**\n * Creates an AsyncLocalStorage-based context system.\n *\n * Returns a middleware (to add to the logger) and helper functions\n * to manage the async context.\n *\n * How it works:\n * 1. Call `runInContext({ requestId, userId }, handler)` at the start of a request\n * 2. The middleware automatically injects that context into every log entry\n * 3. Context propagates across await, setTimeout, Promise.then, etc.\n * 4. Each request gets its own isolated context (no cross-contamination)\n */\nexport function asyncContextMiddleware<\n TMeta = Record<string, unknown>,\n>(): AsyncContextResult<TMeta> {\n const storage = new AsyncLocalStorage<Record<string, unknown>>();\n\n const middleware: LogMiddleware<TMeta> = (entry, next) => {\n const ctx = storage.getStore();\n if (ctx) {\n // Merge async context into entry's metadata\n const meta = entry.meta as Record<string, unknown>;\n for (const [key, value] of Object.entries(ctx)) {\n // Don't overwrite explicitly provided metadata\n if (!(key in meta)) {\n meta[key] = value;\n }\n }\n\n // Also set correlationId if present in context and not already set\n if (ctx.requestId && !entry.correlationId) {\n entry.correlationId = String(ctx.requestId);\n }\n }\n next(entry);\n };\n\n return {\n middleware,\n\n runInContext(context: Record<string, unknown>, fn: () => void): void {\n // If already inside a context, merge with parent\n const parentCtx = storage.getStore();\n const mergedCtx = parentCtx\n ? { ...parentCtx, ...context }\n : { ...context };\n storage.run(mergedCtx, fn);\n },\n\n getContext(): Record<string, unknown> | undefined {\n return storage.getStore();\n },\n\n updateContext(updates: Record<string, unknown>): void {\n const ctx = storage.getStore();\n if (ctx) {\n Object.assign(ctx, updates);\n }\n },\n };\n}\n","/**\n * @module voltlog-io\n * @description Correlation ID middleware — adds tracing IDs to logs.\n * @universal Works in all environments.\n * Useful for tracking requests across microservices.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { LogMiddleware } from \"../core/types.js\";\n\nexport interface CorrelationIdOptions {\n /**\n * Header name or meta key to check for existing ID.\n * Default: 'x-correlation-id' (and checks 'traceId', 'correlationId')\n */\n header?: string;\n /**\n * Function to generate new IDs.\n * Default: cuid2\n */\n generator?: () => string;\n}\n\n/**\n * Middleware that ensures every log entry has a correlation ID.\n * checks:\n * 1. entry.correlationId\n * 2. entry.meta.correlationId\n * 3. entry.meta.traceId\n * 4. entry.meta[header]\n *\n * If none found, generates a new one.\n */\nexport function correlationIdMiddleware<TMeta = Record<string, unknown>>(\n options: CorrelationIdOptions = {},\n): LogMiddleware<TMeta> {\n const header = options.header ?? \"x-correlation-id\";\n const generate = options.generator ?? randomUUID;\n\n return (entry, next) => {\n // 1. Check direct property\n if (entry.correlationId) {\n return next(entry);\n }\n\n // 2. Check meta\n const meta = entry.meta as Record<string, unknown>;\n let id =\n (meta.correlationId as string) ||\n (meta.traceId as string) ||\n (meta[header] as string);\n\n // 3. Generate if missing\n if (!id) {\n id = generate();\n }\n\n // 4. Assign\n entry.correlationId = id;\n\n // Also ensure it's in meta for visibility if desired (optional, but good for transports that only dump meta)\n // We won't overwrite existing keys to avoid side effects, but we can standardize on correlationId\n if (!meta.correlationId) {\n // cast to allows assignment\n (entry.meta as Record<string, unknown>).correlationId = id;\n }\n\n next(entry);\n };\n}\n","/**\n * @module voltlog-io\n * @description File transformer — writes logs to disk with daily rotation and optional size-based rotation.\n * @server-only\n * This transport relies on the `node:fs` module to write files to the local filesystem.\n * Browsers do not have direct access to the filesystem for security reasons.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { LogEntry, LogLevelName, Transport } from \"../core/types.js\";\n\nexport interface FileTransportOptions {\n /** Directory to store logs. Created if missing. */\n dir: string;\n /**\n * Filename pattern. Use `%DATE%` for YYYY-MM-DD.\n * Default: `app-%DATE%.log`\n */\n filename?: string;\n /** Per-transport level filter */\n level?: LogLevelName;\n /**\n * Maximum file size in bytes before rotating to a new file.\n * When exceeded, the current file is renamed with a numeric suffix.\n * Default: no size limit (daily rotation only).\n */\n maxSize?: number;\n}\n\n/**\n * Creates a file transport that writes newline-delimited JSON.\n * Rotates files daily based on the `%DATE%` pattern and optionally by size.\n */\nexport function fileTransport(options: FileTransportOptions): Transport {\n const { dir, level, maxSize } = options;\n const filenamePattern = options.filename ?? \"app-%DATE%.log\";\n\n let currentStream: fs.WriteStream | null = null;\n let currentPath = \"\";\n let currentSize = 0;\n let rotationIndex = 0;\n\n // Date caching — avoid new Date() allocation on every write\n let cachedDate = \"\";\n let cacheExpiry = 0;\n\n function getCachedDate(): string {\n const now = Date.now();\n if (now >= cacheExpiry) {\n cachedDate = new Date(now).toISOString().split(\"T\")[0] as string;\n cacheExpiry = now + 1000; // recompute every second\n }\n return cachedDate;\n }\n\n // Ensure directory exists synchronously on startup\n try {\n fs.mkdirSync(dir, { recursive: true });\n } catch (err) {\n console.error(`[voltlog] Failed to create log directory: ${dir}`, err);\n }\n\n function getPath(sizeRotation = false): string {\n const dateStr = getCachedDate();\n const filename = filenamePattern.replace(\"%DATE%\", dateStr);\n if (sizeRotation && rotationIndex > 0) {\n const ext = path.extname(filename);\n const base = filename.slice(0, -ext.length || undefined);\n return path.join(dir, `${base}.${rotationIndex}${ext}`);\n }\n return path.join(dir, filename);\n }\n\n function openStream(filePath: string): void {\n if (currentStream) {\n currentStream.end();\n }\n currentPath = filePath;\n currentSize = 0;\n currentStream = fs.createWriteStream(filePath, { flags: \"a\" });\n\n // Try to get existing file size for append mode\n try {\n const stat = fs.statSync(filePath);\n currentSize = stat.size;\n } catch {\n // File doesn't exist yet — size stays 0\n }\n\n currentStream.on(\"error\", (err) => {\n console.error(`[voltlog] File write error to ${filePath}:`, err);\n });\n }\n\n function rotate(): void {\n const newPath = getPath();\n if (newPath !== currentPath) {\n rotationIndex = 0;\n openStream(newPath);\n }\n }\n\n // Initialize\n rotate();\n\n return {\n name: \"file\",\n level,\n write(entry: LogEntry): void {\n // Check date-based rotation (uses cached date — very cheap)\n rotate();\n\n const line = `${JSON.stringify(entry)}\\n`;\n\n // Check size-based rotation\n if (maxSize && currentSize + line.length > maxSize) {\n rotationIndex++;\n openStream(getPath(true));\n }\n\n if (currentStream && !currentStream.writableEnded) {\n currentStream.write(line);\n currentSize += line.length;\n }\n },\n async flush(): Promise<void> {\n // No-op for standard fs streams — they handle backpressure internally\n },\n async close(): Promise<void> {\n if (currentStream) {\n return new Promise((resolve) => {\n currentStream?.end(() => resolve());\n });\n }\n },\n };\n}\n","/**\n * @module voltlog-io\n * @description JSON stream transformer — writes newline-delimited JSON to any writable stream.\n * @server-only\n * This transport relies on Node.js-style `Writable` streams (e.g. `process.stdout`, `fs.createWriteStream`).\n * Browser streams (WHATWG Streams API) use a different interface.\n *\n * Useful for file logging, piping to external tools, etc.\n *\n * @example Write to file\n * ```ts\n * import { createWriteStream } from 'node:fs';\n * import { createLogger, jsonStreamTransport } from 'voltlog-io';\n *\n * const logger = createLogger({\n * transports: [\n * jsonStreamTransport({ stream: createWriteStream('./app.log', { flags: 'a' }) }),\n * ],\n * });\n * ```\n *\n * @example Write to stdout\n * ```ts\n * const logger = createLogger({\n * transports: [jsonStreamTransport({ stream: process.stdout })],\n * });\n * ```\n */\n\nimport type { LogEntry, LogLevelName, Transport } from \"../core/types.js\";\n\nexport interface JsonStreamTransportOptions {\n /** Writable stream to output to (e.g. fs.createWriteStream, process.stdout) */\n stream: NodeJS.WritableStream;\n /** Per-transport level filter */\n level?: LogLevelName;\n /**\n * Custom serializer. Return the string to write.\n * Default: JSON.stringify(entry) + '\\n'\n */\n serializer?: (entry: LogEntry) => string;\n}\n\n/**\n * Create a JSON stream transport that writes newline-delimited JSON.\n */\nexport function jsonStreamTransport(\n options: JsonStreamTransportOptions,\n): Transport {\n const stream = options.stream;\n const serialize =\n options.serializer ?? ((entry: LogEntry) => `${JSON.stringify(entry)}\\n`);\n\n return {\n name: \"json-stream\",\n level: options.level,\n write(entry: LogEntry): void {\n const data = serialize(entry);\n stream.write(data);\n },\n close(): Promise<void> {\n return new Promise((resolve) => {\n if (\"end\" in stream && typeof stream.end === \"function\") {\n stream.end(() => resolve());\n } else {\n resolve();\n }\n });\n },\n };\n}\n","/**\n * @module voltlog-io\n * @description Redis Streams transformer — publishes log entries to a Redis Stream.\n * @server-only\n * Designed for standard Redis clients (like `ioredis`) which use TCP sockets (unavailable in browsers).\n * For HTTP-based Redis (e.g. Upstash), use a custom transformer or `webhookTransport`.\n *\n * Users can then subscribe (XREAD/XREADGROUP) for real-time dashboards, monitoring, etc.\n *\n * Requires `ioredis` — user brings their own client instance (no hard dep).\n *\n * @example Basic\n * ```ts\n * import Redis from 'ioredis';\n * import { createLogger, redisTransport } from 'voltlog-io';\n *\n * const redis = new Redis();\n * const logger = createLogger({\n * transports: [\n * redisTransport({ client: redis, streamKey: 'logs:ocpp' }),\n * ],\n * });\n *\n * logger.info('CP connected', { chargePointId: 'CP-101' });\n * // → XADD logs:ocpp * level INFO message \"CP connected\" ...\n * ```\n *\n * @example With TTL and max stream length\n * ```ts\n * redisTransport({\n * client: redis,\n * streamKey: 'logs:ocpp',\n * maxLen: 10_000, // keep last 10k entries (approximate trim)\n * })\n * ```\n *\n * @example Subscribing (consumer side)\n * ```ts\n * // In your dashboard / monitoring service:\n * const redis = new Redis();\n *\n * // Read new entries from the stream\n * const entries = await redis.xread('BLOCK', 0, 'STREAMS', 'logs:ocpp', '$');\n *\n * // Or use consumer groups for at-least-once delivery:\n * await redis.xgroup('CREATE', 'logs:ocpp', 'dashboard', '$', 'MKSTREAM');\n * const entries = await redis.xreadgroup(\n * 'GROUP', 'dashboard', 'worker-1',\n * 'BLOCK', 0, 'STREAMS', 'logs:ocpp', '>'\n * );\n * ```\n */\n\nimport type { LogEntry, LogLevelName, Transport } from \"../core/types.js\";\n\n/**\n * Minimal interface for the Redis client methods we need.\n * Compatible with `ioredis` — user provides their own instance.\n */\nexport interface RedisClient {\n xadd(...args: (string | number)[]): Promise<string | null>;\n quit?(): Promise<void>;\n}\n\nexport interface RedisTransportOptions {\n /** Redis client instance (e.g. `new Redis()` from ioredis) */\n client: RedisClient;\n /** Redis Stream key to publish to (default: 'logs') */\n streamKey?: string;\n /**\n * Max approximate stream length — older entries are trimmed.\n * Uses MAXLEN ~ (approximate) to keep the stream bounded.\n * Default: no limit.\n */\n maxLen?: number;\n /** Per-transport level filter */\n level?: LogLevelName;\n /**\n * Custom field mapper — convert LogEntry to flat key-value pairs for Redis.\n * Default: serializes the entire entry as JSON under a single 'data' field.\n */\n fieldMapper?: (entry: LogEntry) => Record<string, string>;\n}\n\n/**\n * Create a Redis Streams transport.\n *\n * Publishes each log entry via XADD to a Redis Stream.\n * Fire-and-forget — errors are silently swallowed to avoid blocking.\n */\nexport function redisTransport(options: RedisTransportOptions): Transport {\n const { client, streamKey = \"logs\", maxLen, level } = options;\n\n const fieldMapper = options.fieldMapper ?? defaultFieldMapper;\n\n function defaultFieldMapper(entry: LogEntry): Record<string, string> {\n return {\n id: entry.id,\n level: String(entry.level),\n levelName: entry.levelName,\n message: entry.message,\n timestamp: String(entry.timestamp),\n data: JSON.stringify({\n meta: entry.meta,\n context: entry.context,\n correlationId: entry.correlationId,\n error: entry.error,\n }),\n };\n }\n\n return {\n name: \"redis\",\n level,\n write(entry: LogEntry): void {\n const fields = fieldMapper(entry);\n const args: (string | number)[] = [streamKey];\n\n // Approximate trimming to bound stream length\n if (maxLen) {\n args.push(\"MAXLEN\", \"~\", maxLen);\n }\n\n args.push(\"*\"); // auto-generate stream entry ID\n\n // Flatten fields into [key, value, key, value, ...]\n for (const [key, value] of Object.entries(fields)) {\n args.push(key, value);\n }\n\n // Fire-and-forget\n client.xadd(...args).catch(() => {\n /* Swallowed — never crash the app */\n });\n },\n async close(): Promise<void> {\n // Don't close the user's Redis client — they own it\n },\n };\n}\n"],"mappings":"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","names":[]}