@onezlinks/session-logger 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-02-07
9
+
10
+ ### Added
11
+
12
+ - **Hybrid API** — Wrapper pattern (`withSession`) + Manual pattern (`startSession`/`endSession`)
13
+ - **AsyncLocalStorage** context propagation — implicit session context without parameter drilling
14
+ - **Dual output** — simultaneous file + stdout logging
15
+ - **Category-based organization** — `logs/{category}/YYYY-MM-DD/{sessionId}_{timestamp}.log`
16
+ - **File logger factory** — `console.Console` with separate `.log` and `.err.log` streams
17
+ - **Cost/duration/metric formatters** — consistent structured output
18
+ - **Log rotation** — `cleanOldLogs()` with configurable retention period
19
+ - **Graceful fallback** — logs to console when outside session context or on file errors
20
+ - **Zero dependencies** — uses only Node.js built-in modules
21
+ - **TypeScript definitions** — full `.d.ts` type support
22
+ - **Environment variable configuration** — `SESSION_LOG_DIR`, `SESSION_LOG_RETENTION_DAYS`, etc.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Onez (onezlinks)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,648 @@
1
+ # @onezlinks/session-logger
2
+
3
+ > 📁 Session-based file logger with **AsyncLocalStorage** context propagation for Node.js microservices.
4
+
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen)](https://nodejs.org/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-blue)](package.json)
8
+
9
+ ---
10
+
11
+ ## 📑 Table of Contents
12
+
13
+ - [Features](#-features)
14
+ - [Installation](#-installation)
15
+ - [Quick Start](#-quick-start)
16
+ - [How It Works](#-how-it-works)
17
+ - [API Reference](#-api-reference)
18
+ - [Session Management](#session-management)
19
+ - [Logging Functions](#logging-functions)
20
+ - [Formatters](#formatters)
21
+ - [Utilities](#utilities)
22
+ - [Constants](#constants)
23
+ - [Log Output](#-log-output)
24
+ - [Directory Structure](#directory-structure)
25
+ - [File Naming Convention](#file-naming-convention)
26
+ - [Log File Content](#log-file-content)
27
+ - [Configuration](#%EF%B8%8F-configuration)
28
+ - [Log Rotation & Cleanup](#-log-rotation--cleanup)
29
+ - [Docker](#-docker)
30
+ - [TypeScript Support](#-typescript-support)
31
+ - [Testing](#-testing)
32
+ - [License](#-license)
33
+
34
+ ---
35
+
36
+ ## ✨ Features
37
+
38
+ | Feature | Description |
39
+ | ---------------------------- | -------------------------------------------------------------- |
40
+ | 🔄 **Hybrid API** | Wrapper (`withSession`) + Manual (`startSession`/`endSession`) |
41
+ | 🧵 **AsyncLocalStorage** | Implicit context propagation — no parameter drilling |
42
+ | 📝 **Dual Output** | Simultaneous file + stdout/stderr logging |
43
+ | 📂 **Category-based** | Logs organized into `{category}/YYYY-MM-DD/` subdirectories |
44
+ | 🧹 **Log Rotation** | `cleanOldLogs()` with configurable retention period |
45
+ | 💰 **Structured Formatters** | Cost, duration, and custom metric formatters |
46
+ | 🛡️ **Graceful Fallback** | Falls back to `console.*` when called outside a session |
47
+ | 📦 **Zero Dependencies** | Uses only Node.js built-in modules |
48
+ | 🔤 **TypeScript Support** | Full `.d.ts` type definitions included |
49
+
50
+ ---
51
+
52
+ ## 📦 Installation
53
+
54
+ ```bash
55
+ # Published package (after npm publish)
56
+ npm install @onezlinks/session-logger
57
+ ```
58
+
59
+ For local development (monorepo or pre-publish):
60
+
61
+ ```bash
62
+ # Install from sibling directory
63
+ npm install ../session-logger
64
+
65
+ # Or use npm link
66
+ cd /path/to/session-logger && npm link
67
+ cd /path/to/your-service && npm link @onezlinks/session-logger
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 🚀 Quick Start
73
+
74
+ ### Wrapper Pattern (Recommended)
75
+
76
+ `withSession` creates a session context, runs your function, and automatically writes a summary footer and closes file streams when done.
77
+
78
+ ```javascript
79
+ const { withSession, log, logCost } = require("@onezlinks/session-logger");
80
+
81
+ async function processOrder(order) {
82
+ await withSession(
83
+ {
84
+ sessionId: order._id.toString(),
85
+ category: "order-processing",
86
+ metadata: { orderId: order._id, store: order.storeId },
87
+ },
88
+ async () => {
89
+ log("Pipeline", "🚀 Processing started");
90
+
91
+ // ... your business logic ...
92
+ // All log/error/warn calls within this async scope
93
+ // automatically write to the session log file.
94
+
95
+ logCost("Billing", { totalUSD: 0.001, totalTHB: 0.035 }, 100, 50);
96
+ log("Pipeline", "✅ Processing complete");
97
+ },
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### Manual Pattern
103
+
104
+ Use `startSession` / `endSession` when you need finer control over the session lifecycle (e.g., event handlers, iterative flows). You **must** call `endSession()` in a `finally` block — otherwise the file streams will leak.
105
+
106
+ ```javascript
107
+ const {
108
+ startSession,
109
+ endSession,
110
+ log,
111
+ error,
112
+ } = require("@onezlinks/session-logger");
113
+
114
+ async function handleWebhook(data) {
115
+ startSession({
116
+ sessionId: data.id,
117
+ category: "webhook",
118
+ });
119
+
120
+ try {
121
+ log("Webhook", "Received payload");
122
+ // ... processing ...
123
+ } catch (err) {
124
+ error("Webhook", `Failed: ${err.message}`);
125
+ throw err;
126
+ } finally {
127
+ await endSession(); // ⚠️ MUST be in finally block
128
+ }
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## 🔍 How It Works
135
+
136
+ ```text
137
+ ┌───────────────────────────────────────────────────────────┐
138
+ │ withSession(options, asyncFn) │
139
+ │ ┌─────────────────────────────────────────────────────┐ │
140
+ │ │ 1. Create date folder logs/{category}/YYYY-MM-DD │ │
141
+ │ │ 2. Open write streams .log + .err.log │ │
142
+ │ │ 3. Write session header │ │
143
+ │ │ 4. Store context in AsyncLocalStorage │ │
144
+ │ │ 5. Execute asyncFn() │ │
145
+ │ │ ┌──────────────────────────────────────────┐ │ │
146
+ │ │ │ log() → .log file + stdout │ │ │
147
+ │ │ │ warn() → .log file + stdout │ │ │
148
+ │ │ │ error() → .err.log file + stderr │ │ │
149
+ │ │ └──────────────────────────────────────────┘ │ │
150
+ │ │ 6. Write session summary (always "SUCCESS") │ │
151
+ │ │ 7. Close file streams │ │
152
+ │ └─────────────────────────────────────────────────────┘ │
153
+ └───────────────────────────────────────────────────────────┘
154
+ ```
155
+
156
+ **Key behaviors:**
157
+
158
+ - `log()` and `warn()` write to the `.log` file via `stdout` stream of `console.Console`.
159
+ - `error()` writes to a **separate** `.err.log` file via `stderr` stream.
160
+ - When called **outside** a session context, all functions fall back to the global `console` object (`console.log`, `console.error`, `console.warn`).
161
+ - The session summary always records status `SUCCESS`. Error tracking is available through `stats.errors` count.
162
+
163
+ ---
164
+
165
+ ## 📖 API Reference
166
+
167
+ ### Session Management
168
+
169
+ #### `withSession(options, asyncFn) → Promise<T>`
170
+
171
+ Run an async function within a session context. The session is automatically finalized in a `finally` block, ensuring streams are always closed — even if `asyncFn` throws.
172
+
173
+ | Parameter | Type | Required | Default | Description |
174
+ | ---------------------- | ------------------ | -------- | ---------------------- | -------------------------------------------- |
175
+ | `options.sessionId` | `string` | ✅ | — | Unique session identifier |
176
+ | `options.category` | `string` | ✅ | — | Log category — used as subdirectory name |
177
+ | `options.metadata` | `object` | ❌ | `{}` | Key-value pairs added to the session header |
178
+ | `options.logDir` | `string` | ❌ | `CONFIG.LOG_BASE_DIR` | Override base log directory |
179
+ | `options.enableFile` | `boolean` | ❌ | `CONFIG.ENABLE_FILE` | Enable/disable file logging |
180
+ | `options.enableStdout` | `boolean` | ❌ | `CONFIG.ENABLE_STDOUT` | Enable/disable stdout/stderr output |
181
+ | `asyncFn` | `() => Promise<T>` | ✅ | — | Async function to execute within the session |
182
+
183
+ **Returns:** The return value of `asyncFn`.
184
+
185
+ ```javascript
186
+ const result = await withSession(
187
+ { sessionId: "abc123", category: "ai-pipeline" },
188
+ async () => {
189
+ log("Pipeline", "Starting...");
190
+ return { success: true };
191
+ },
192
+ );
193
+ // result === { success: true }
194
+ ```
195
+
196
+ ---
197
+
198
+ #### `startSession(options) → void`
199
+
200
+ Start a session context using the manual pattern. Accepts the **same options** as `withSession` (without `asyncFn`).
201
+
202
+ > ⚠️ You **must** call `endSession()` in a `finally` block. Failing to do so will leave file streams open (resource leak).
203
+
204
+ ---
205
+
206
+ #### `endSession() → Promise<void>`
207
+
208
+ End the current session — writes the summary footer and closes file streams. If called outside a session context, this is a no-op.
209
+
210
+ ---
211
+
212
+ #### `getSessionContext() → SessionContext | null`
213
+
214
+ Returns the current session context object, or `null` if not inside a session. Useful for conditional logic based on session state.
215
+
216
+ ```javascript
217
+ const ctx = getSessionContext();
218
+ if (ctx) {
219
+ console.log(`Session ${ctx.sessionId} has ${ctx.stats.errors} errors`);
220
+ }
221
+ ```
222
+
223
+ **`SessionContext` properties:**
224
+
225
+ | Property | Type | Description |
226
+ | ------------ | ---------------- | ---------------------------------------------------- |
227
+ | `sessionId` | `string` | The session identifier |
228
+ | `category` | `string` | The log category |
229
+ | `metadata` | `object` | Metadata from options |
230
+ | `logPath` | `string \| null` | Absolute path to the `.log` file |
231
+ | `errLogPath` | `string \| null` | Absolute path to the `.err.log` file |
232
+ | `startTime` | `number` | `Date.now()` when the session started |
233
+ | `stats` | `object` | `{ logs: number, errors: number, warnings: number }` |
234
+
235
+ ---
236
+
237
+ ### Logging Functions
238
+
239
+ All logging functions follow this signature:
240
+
241
+ ```javascript
242
+ functionName(tag, message, ...args);
243
+ ```
244
+
245
+ - **`tag`** — A short label for the source module/component (padded to 12 characters in output).
246
+ - **`message`** — The log message string.
247
+ - **`...args`** — Additional values (objects, arrays, etc.) appended after the message.
248
+
249
+ **Output behavior:**
250
+
251
+ | Context | `enableStdout` | File output | Console output |
252
+ | --------------- | -------------- | ----------- | ---------------------------- |
253
+ | Inside session | `true` | ✅ Writes | ✅ Writes |
254
+ | Inside session | `false` | ✅ Writes | ❌ Suppressed |
255
+ | Outside session | — | ❌ No file | ✅ Falls back to `console.*` |
256
+
257
+ **Output format:**
258
+
259
+ ```text
260
+ [HH:mm:ss.SSS] [Tag ] message
261
+ ```
262
+
263
+ ---
264
+
265
+ #### `log(tag, message, ...args)`
266
+
267
+ Log an info-level message. Writes to the `.log` file and `console.log`.
268
+
269
+ ```javascript
270
+ log("Pipeline", "🚀 Processing started");
271
+ log("OCR", "Result:", { confidence: 0.95 });
272
+ ```
273
+
274
+ #### `info(tag, message, ...args)`
275
+
276
+ Alias for `log()`. Use whichever reads better in your code.
277
+
278
+ #### `warn(tag, message, ...args)`
279
+
280
+ Log a warning message. Writes to the `.log` file (same as `log`) and `console.warn`. Increments `stats.warnings`.
281
+
282
+ #### `error(tag, message, ...args)`
283
+
284
+ Log an error message. Writes to a **separate `.err.log` file** (not the main `.log`) and `console.error`. Increments `stats.errors`.
285
+
286
+ ---
287
+
288
+ ### Formatters
289
+
290
+ Convenience functions that format data into a consistent string and log it via `log()`.
291
+
292
+ #### `logCost(tag, costData, inputUnits?, outputUnits?, cachedUnits?)`
293
+
294
+ Log cost/billing data.
295
+
296
+ | Parameter | Type | Default | Description |
297
+ | ------------- | -------- | ------- | ---------------------------------------- |
298
+ | `tag` | `string` | — | Module/component tag |
299
+ | `costData` | `object` | — | `{ totalUSD: number, totalTHB: number }` |
300
+ | `inputUnits` | `number` | `0` | Input units (tokens, API calls) |
301
+ | `outputUnits` | `number` | `0` | Output units |
302
+ | `cachedUnits` | `number` | `0` | Cached units |
303
+
304
+ ```javascript
305
+ logCost("Quality", { totalUSD: 0.001, totalTHB: 0.035 }, 100, 50);
306
+ // → [12:00:00.500] [Quality ] 💰 Cost: $0.001000 (~฿0.0350) | Units: 100+50
307
+
308
+ logCost("Quality", { totalUSD: 0.002, totalTHB: 0.07 }, 200, 80, 50);
309
+ // → [12:00:00.500] [Quality ] 💰 Cost: $0.002000 (~฿0.0700) | Units: 200+80 (cached: 50)
310
+ ```
311
+
312
+ #### `logDuration(tag, durationMs, label?)`
313
+
314
+ Log a duration metric.
315
+
316
+ | Parameter | Type | Default | Description |
317
+ | ------------ | -------- | ------------ | ------------------------ |
318
+ | `tag` | `string` | — | Module/component tag |
319
+ | `durationMs` | `number` | — | Duration in milliseconds |
320
+ | `label` | `string` | `'Duration'` | Metric label |
321
+
322
+ ```javascript
323
+ logDuration("Pipeline", 500);
324
+ // → [12:00:01.200] [Pipeline ] ⏱️ Duration: 500ms
325
+
326
+ logDuration("Pipeline", 2500);
327
+ // → [12:00:01.200] [Pipeline ] ⏱️ Duration: 2500ms (2.50s)
328
+
329
+ logDuration("OCR", 3200, "OCR Time");
330
+ // → [12:00:01.200] [OCR ] ⏱️ OCR Time: 3200ms (3.20s)
331
+ ```
332
+
333
+ > **Note:** The seconds conversion `(X.XXs)` only appears when the duration is ≥ 1000ms.
334
+
335
+ #### `logMetric(tag, metricName, value, unit?)`
336
+
337
+ Log a custom metric.
338
+
339
+ ```javascript
340
+ logMetric("OCR", "Confidence", 95.5, "%");
341
+ // → [12:00:01.200] [OCR ] Confidence: 95.5 %
342
+
343
+ logMetric("Pipeline", "Items Processed", 42);
344
+ // → [12:00:01.200] [Pipeline ] Items Processed: 42
345
+ ```
346
+
347
+ ---
348
+
349
+ ### Utilities
350
+
351
+ #### `cleanOldLogs(category?, daysToKeep?) → Promise<CleanupResult>`
352
+
353
+ Remove date-based log folders older than the retention period. Scans `{LOG_BASE_DIR}/{category}/YYYY-MM-DD/` and removes folders where the date is older than `daysToKeep`.
354
+
355
+ | Parameter | Type | Default | Description |
356
+ | ------------ | -------- | --------------------------- | -------------------------- |
357
+ | `category` | `string` | all categories | Specific category to clean |
358
+ | `daysToKeep` | `number` | `CONFIG.LOG_RETENTION_DAYS` | Number of days to retain |
359
+
360
+ **Returns:** `{ deleted: string[], errors: string[] }`
361
+
362
+ ```javascript
363
+ const { cleanOldLogs } = require("@onezlinks/session-logger");
364
+
365
+ // Clean all categories using default retention (30 days)
366
+ const result = await cleanOldLogs();
367
+ console.log(`Deleted ${result.deleted.length} folders`);
368
+ // deleted: ['ai-pipeline/2026-01-01', 'webhook/2026-01-02', ...]
369
+
370
+ // Clean a specific category, keep only last 7 days
371
+ const result = await cleanOldLogs("ai-pipeline", 7);
372
+ ```
373
+
374
+ If the base log directory does not exist, returns `{ deleted: [], errors: [] }` without throwing.
375
+
376
+ ---
377
+
378
+ ### Constants
379
+
380
+ #### `CONFIG`
381
+
382
+ Configuration object with **lazy evaluation** — environment variables are read at access time (not at import time), making them testable and overridable at runtime.
383
+
384
+ | Property | Env Variable | Default | Description |
385
+ | -------------------- | ---------------------------- | -------- | ------------------------------------------------ |
386
+ | `LOG_BASE_DIR` | `SESSION_LOG_DIR` | `'logs'` | Base directory for log files (relative to `cwd`) |
387
+ | `LOG_RETENTION_DAYS` | `SESSION_LOG_RETENTION_DAYS` | `30` | Days to retain logs before cleanup |
388
+ | `ENABLE_FILE` | `SESSION_LOG_TO_FILE` | `true` | Set to `'false'` to disable file logging |
389
+ | `ENABLE_STDOUT` | `SESSION_LOG_TO_STDOUT` | `true` | Set to `'false'` to disable stdout/stderr output |
390
+
391
+ > **Note:** `ENABLE_FILE` and `ENABLE_STDOUT` are only disabled when the env var is **exactly** `'false'` (string). Any other value (including unset) keeps them enabled.
392
+
393
+ #### `COMMON_TAGS`
394
+
395
+ Predefined tag string constants for consistency across services. These are suggestions — you can use any string as a tag.
396
+
397
+ | Group | Tags |
398
+ | --------------- | ---------------------------------------------------- |
399
+ | **AI Pipeline** | `PIPELINE`, `QUALITY`, `OCR`, `OPENAI`, `CLIENT` |
400
+ | **Rewards** | `REDEMPTION`, `POINTS`, `VALIDATION`, `NOTIFICATION` |
401
+ | **Survey** | `SURVEY`, `STORAGE`, `ANALYTICS` |
402
+ | **Order** | `ORDER`, `PAYMENT`, `INVENTORY`, `SHIPPING` |
403
+ | **Messaging** | `MESSAGE`, `WEBHOOK`, `RESPONSE`, `BROADCAST` |
404
+ | **Generic** | `DATABASE`, `CACHE`, `WORKER` |
405
+
406
+ ```javascript
407
+ const { COMMON_TAGS, log } = require("@onezlinks/session-logger");
408
+
409
+ log(COMMON_TAGS.PIPELINE, "Processing started"); // tag = 'Pipeline'
410
+ log(COMMON_TAGS.OCR, "Recognition complete"); // tag = 'OCR'
411
+ log(COMMON_TAGS.DATABASE, "Query executed"); // tag = 'Database'
412
+ ```
413
+
414
+ ---
415
+
416
+ ## 📂 Log Output
417
+
418
+ ### Directory Structure
419
+
420
+ ```text
421
+ logs/ ← CONFIG.LOG_BASE_DIR
422
+ ├── ai-pipeline/ ← category
423
+ │ └── 2026-02-07/ ← date folder (YYYY-MM-DD)
424
+ │ ├── 6789abc_20260207T120000_001.log
425
+ │ └── 6789abc_20260207T120000_001.err.log
426
+ └── order-processing/
427
+ └── 2026-02-07/
428
+ ├── def5678_20260207T130000_042.log
429
+ └── def5678_20260207T130000_042.err.log
430
+ ```
431
+
432
+ ### File Naming Convention
433
+
434
+ Each session creates a unique filename:
435
+
436
+ ```text
437
+ {sessionId7}_{timestamp}_{random}.log
438
+ ```
439
+
440
+ | Component | Description | Example |
441
+ | ------------ | ---------------------------------------------- | ----------------- |
442
+ | `sessionId7` | First 7 characters of `sessionId` | `6789abc` |
443
+ | `timestamp` | ISO 8601 compact format (no dashes, no colons) | `20260207T120000` |
444
+ | `random` | 3-digit random number (avoids collisions) | `001` |
445
+
446
+ This produces filenames like `6789abc_20260207T120000_001.log` and a matching `.err.log`.
447
+
448
+ ### Log File Content
449
+
450
+ ```text
451
+ ═══════════════════════════════════════════════════════════════
452
+ Ai Pipeline Session
453
+ ═══════════════════════════════════════════════════════════════
454
+ Session ID : 6789abc...
455
+ Category : ai-pipeline
456
+ Order Id : order-001
457
+ Started : 2026-02-07T12:00:00.000Z
458
+ ═══════════════════════════════════════════════════════════════
459
+
460
+ [12:00:00.001] [Pipeline ] 🚀 Processing started
461
+ [12:00:00.500] [Quality ] 💰 Cost: $0.001000 (~฿0.0350) | Units: 100+50
462
+ [12:00:01.200] [Pipeline ] ✅ Processing complete
463
+
464
+ ═══════════════════════════════════════════════════════════════
465
+ Session Summary
466
+ ═══════════════════════════════════════════════════════════════
467
+ Status : SUCCESS
468
+ Duration : 1200ms (1.20s)
469
+ Log Stats : 3 info, 0 errors, 0 warnings
470
+ ═══════════════════════════════════════════════════════════════
471
+ ```
472
+
473
+ **Notes:**
474
+
475
+ - The **session title** is derived from the `category` by converting `kebab-case` to `Title Case` (e.g., `ai-pipeline` → `Ai Pipeline Session`).
476
+ - **Metadata keys** are formatted from `camelCase` to `Title Case` (e.g., `orderId` → `Order Id`).
477
+ - The **summary status** is always `SUCCESS`. To detect errors, check the `stats.errors` count via `getSessionContext()` or inspect the `.err.log` file.
478
+
479
+ ---
480
+
481
+ ## ⚙️ Configuration
482
+
483
+ All configuration is through **environment variables**. No config files needed.
484
+
485
+ ```bash
486
+ # Change log directory (default: 'logs')
487
+ SESSION_LOG_DIR=./output/logs
488
+
489
+ # Change retention period (default: 30 days)
490
+ SESSION_LOG_RETENTION_DAYS=14
491
+
492
+ # Disable file logging (only stdout)
493
+ SESSION_LOG_TO_FILE=false
494
+
495
+ # Disable stdout (only file logging — useful in production)
496
+ SESSION_LOG_TO_STDOUT=false
497
+ ```
498
+
499
+ You can also override per-session:
500
+
501
+ ```javascript
502
+ await withSession(
503
+ {
504
+ sessionId: "test-123",
505
+ category: "debug",
506
+ logDir: "./debug-logs", // Override LOG_BASE_DIR for this session
507
+ enableFile: false, // Skip file logging for this session
508
+ enableStdout: true, // Still print to console
509
+ },
510
+ async () => {
511
+ log("Debug", "This only prints to console, no file created");
512
+ },
513
+ );
514
+ ```
515
+
516
+ ---
517
+
518
+ ## 🧹 Log Rotation & Cleanup
519
+
520
+ The library provides a `cleanOldLogs()` function that removes date folders older than the retention period. **Scheduling is your responsibility** — the library intentionally does not include a built-in scheduler to remain zero-dependency.
521
+
522
+ ### Startup Cleanup (Simplest)
523
+
524
+ ```javascript
525
+ const { cleanOldLogs } = require("@onezlinks/session-logger");
526
+
527
+ // Run once at application startup
528
+ cleanOldLogs().then(({ deleted, errors }) => {
529
+ if (deleted.length)
530
+ console.log(`[Cleanup] Removed ${deleted.length} old log folders`);
531
+ if (errors.length) console.error("[Cleanup] Errors:", errors);
532
+ });
533
+ ```
534
+
535
+ ### Scheduled Cleanup with `node-cron`
536
+
537
+ ```bash
538
+ npm install node-cron
539
+ ```
540
+
541
+ ```javascript
542
+ const cron = require("node-cron");
543
+ const { cleanOldLogs } = require("@onezlinks/session-logger");
544
+
545
+ // Run daily at 3:00 AM
546
+ cron.schedule("0 3 * * *", async () => {
547
+ const { deleted, errors } = await cleanOldLogs();
548
+ if (deleted.length)
549
+ console.log(`[Cron] Deleted ${deleted.length} old log folders`);
550
+ if (errors.length) console.error("[Cron] Cleanup errors:", errors);
551
+ });
552
+ ```
553
+
554
+ ### Scheduled Cleanup with `setInterval` (No Extra Dependencies)
555
+
556
+ ```javascript
557
+ const { cleanOldLogs } = require("@onezlinks/session-logger");
558
+
559
+ // Run every 24 hours
560
+ setInterval(
561
+ async () => {
562
+ const { deleted } = await cleanOldLogs();
563
+ if (deleted.length)
564
+ console.log(`[Cleanup] Deleted ${deleted.length} folders`);
565
+ },
566
+ 24 * 60 * 60 * 1000,
567
+ );
568
+ ```
569
+
570
+ ### Cluster-Safe Pattern (PM2 / Docker Swarm)
571
+
572
+ When running multiple instances, schedule cleanup on **one instance only**:
573
+
574
+ ```javascript
575
+ const cluster = require("cluster");
576
+ const cron = require("node-cron");
577
+ const { cleanOldLogs } = require("@onezlinks/session-logger");
578
+
579
+ if (cluster.isPrimary || process.env.NODE_APP_INSTANCE === "0") {
580
+ cron.schedule("0 3 * * *", () => cleanOldLogs());
581
+ }
582
+ ```
583
+
584
+ ### Cron Schedule Reference
585
+
586
+ | Pattern | Description |
587
+ | ------------- | ----------------------------- |
588
+ | `0 3 * * *` | Every day at 3:00 AM |
589
+ | `0 */6 * * *` | Every 6 hours |
590
+ | `0 0 * * 0` | Every Sunday at midnight |
591
+ | `0 2 1 * *` | First day of month at 2:00 AM |
592
+
593
+ ---
594
+
595
+ ## 🐳 Docker
596
+
597
+ Add a volume mount to persist logs outside the container:
598
+
599
+ ```yaml
600
+ services:
601
+ your-service:
602
+ volumes:
603
+ - ./logs:/app/logs
604
+ environment:
605
+ - SESSION_LOG_DIR=logs # relative to /app (working dir)
606
+ - SESSION_LOG_RETENTION_DAYS=14
607
+ ```
608
+
609
+ > Ensure the `SESSION_LOG_DIR` path matches the volume mount target. The default value `logs` works with `/app/logs` when the working directory is `/app`.
610
+
611
+ ---
612
+
613
+ ## 🔤 TypeScript Support
614
+
615
+ Full type definitions are included at `types/index.d.ts`. No additional `@types/` package is needed.
616
+
617
+ ```typescript
618
+ import {
619
+ withSession,
620
+ log,
621
+ error,
622
+ logCost,
623
+ cleanOldLogs,
624
+ CONFIG,
625
+ COMMON_TAGS,
626
+ type SessionOptions,
627
+ type SessionContext,
628
+ type CostData,
629
+ type CleanupResult,
630
+ } from "@onezlinks/session-logger";
631
+ ```
632
+
633
+ ---
634
+
635
+ ## 🧪 Testing
636
+
637
+ ```bash
638
+ npm test # Run all tests with coverage
639
+ npm run test:watch # Watch mode for development
640
+ npm run lint # ESLint check
641
+ npm run lint:fix # ESLint auto-fix
642
+ ```
643
+
644
+ ---
645
+
646
+ ## 📄 License
647
+
648
+ [MIT](LICENSE) © Onez (onezlinks)