@reliverse/relinka 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +295 -210
- package/bin/impl.d.ts +28 -16
- package/bin/impl.js +91 -37
- package/bin/mod.d.ts +2 -2
- package/bin/mod.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# Relinka: Logging that Actually Feels Good
|
|
2
2
|
|
|
3
|
-
> **@reliverse/relinka** is a modern, minimal logging library that actually *feels* right. It's not just pretty output — it's a system: smart formatting, file-safe logging, runtime config support, and a `fatal` mode built for developers who care about correctness. Whether you're building CLI tools, SDKs, or full-stack apps — Relinka helps you log with intention.
|
|
4
|
-
|
|
5
3
|
[sponsor](https://github.com/sponsors/blefnk) — [discord](https://discord.gg/Pb8uKbwpsJ) — [repo](https://github.com/reliverse/relinka) — [npm](https://npmjs.com/@reliverse/relinka)
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
> **@reliverse/relinka** is a modern logging library that actually *feels* right. It's not just pretty output — it's a system: smart formatting, file-safe logging, runtime config support, and a `fatal` mode built for developers who care about correctness. Whether you're building CLI tools, SDKs, or full-stack apps — Relinka helps you log with intention.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
- 💬 Supports: `info`, `warn`, `success`, `error`, `verbose`, `fatal`, `clear`
|
|
11
|
-
- 🎨 Terminal output that *pops*, with automatic color handling
|
|
12
|
-
- 📁 Save logs to file (with daily rotation, cleanup, and max-file limits)
|
|
13
|
-
- 🧠 Structured message formatting (objects, errors, stacks — handled!)
|
|
14
|
-
- ⚙️ Runtime config via `relinka.config.ts` (powered by `reconf`)
|
|
15
|
-
- 🚨 `fatal` logs halt execution and trigger `debugger` in dev
|
|
16
|
-
- 🧩 Sync-first, async, & CLI-friendly thanks to buffer flushing
|
|
7
|
+
## Why Relinka
|
|
17
8
|
|
|
18
|
-
|
|
9
|
+
- 🧙 **Drop-in replacement** for `node:console`, `consola`, or your internal logger
|
|
10
|
+
- 💬 **12 log levels**: `info`, `warn`, `success`, `error`, `verbose`, `fatal`, `log`, `step`, `box`, `message`, `internal`, `null`
|
|
11
|
+
- 🎨 **Beautiful terminal output** with automatic color handling and Unicode support
|
|
12
|
+
- 📁 **Smart file logging** with buffering, rotation, cleanup, and date-based naming
|
|
13
|
+
- 🧠 **Structured formatting** for objects, errors, and stack traces
|
|
14
|
+
- ⚙️ **Runtime configuration** via `.config/relinka.ts` or `relinka.config.ts`
|
|
15
|
+
- 🚨 **Fatal logging** that halts execution and triggers debugger in development
|
|
16
|
+
- 🧩 **Dual syntax support** - both function calls (`relinka("info", "Hello world")`) and method chaining (`relinka.info("Hello world")`)
|
|
17
|
+
- ⚡ **Performance optimized** with intelligent buffering and async support
|
|
18
|
+
- 🛎️ **Dler-ready** - if you use `dler build`, all `internal`-level logs will be removed from the built dist
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
## Quick Start
|
|
21
21
|
|
|
22
22
|
### 1. Install
|
|
23
23
|
|
|
@@ -25,158 +25,173 @@ Make sure you have git, node.js, and bun/pnpm/yarn/npm installed.
|
|
|
25
25
|
bun add @reliverse/relinka
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### 2. Basic Usage
|
|
29
29
|
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
```ts
|
|
31
|
+
import { relinka, relinkaConfig, relinkaShutdown } from "@reliverse/relinka";
|
|
32
|
+
|
|
33
|
+
async function main() {
|
|
34
|
+
// Load configuration (required at start)
|
|
35
|
+
await relinkaConfig();
|
|
36
|
+
|
|
37
|
+
// Log with different levels
|
|
38
|
+
relinka("info", "Application started");
|
|
39
|
+
relinka("success", "Configuration loaded successfully");
|
|
40
|
+
relinka("warn", "This is a warning");
|
|
41
|
+
relinka("error", "Something went wrong");
|
|
42
|
+
|
|
43
|
+
// Use method syntax (also supported)
|
|
44
|
+
relinka.info("Another info message");
|
|
45
|
+
relinka.success("Another success message");
|
|
46
|
+
|
|
47
|
+
// Clean shutdown (required at end)
|
|
48
|
+
await relinkaShutdown();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main();
|
|
33
52
|
```
|
|
34
53
|
|
|
35
|
-
|
|
54
|
+
## Log Levels
|
|
55
|
+
|
|
56
|
+
Relinka supports 12 different log levels, each with customizable symbols and colors:
|
|
57
|
+
|
|
58
|
+
| Level | Symbol | Description |
|
|
59
|
+
|-------|--------|-------------|
|
|
60
|
+
| `info` | ◈ | General information |
|
|
61
|
+
| `success` | ✓ | Success messages |
|
|
62
|
+
| `warn` | ⚠ | Warnings |
|
|
63
|
+
| `error` | ✖ | Non-fatal errors |
|
|
64
|
+
| `fatal` | ‼ | Fatal errors + error throwing |
|
|
65
|
+
| `verbose` | ✱ | Debug information |
|
|
66
|
+
| `log` | │ | General logging |
|
|
67
|
+
| `step` | → | Progress steps |
|
|
68
|
+
| `box` | ■ | Boxed messages |
|
|
69
|
+
| `message` | 🞠 | General messages |
|
|
70
|
+
| `internal` | ⚙ | Internal system logs |
|
|
71
|
+
| `null` | (none) | No symbol or spacing |
|
|
36
72
|
|
|
37
|
-
|
|
73
|
+
## API Reference
|
|
38
74
|
|
|
39
|
-
|
|
75
|
+
### Core Functions
|
|
76
|
+
|
|
77
|
+
#### `relinka(level, message, ...args)`
|
|
78
|
+
|
|
79
|
+
Main logging function with dual syntax support.
|
|
40
80
|
|
|
41
81
|
```ts
|
|
42
|
-
|
|
82
|
+
// Function syntax
|
|
83
|
+
relinka("info", "Hello world");
|
|
84
|
+
relinka("error", "Something broke", { error: "details" });
|
|
85
|
+
|
|
86
|
+
// Method syntax
|
|
87
|
+
relinka.info("Hello world");
|
|
88
|
+
relinka.error("Something broke", { error: "details" });
|
|
43
89
|
```
|
|
44
90
|
|
|
45
|
-
|
|
91
|
+
#### `relinkaAsync(level, message, ...args)`
|
|
92
|
+
|
|
93
|
+
Async version that waits for configuration to load.
|
|
46
94
|
|
|
47
95
|
```ts
|
|
48
|
-
await
|
|
96
|
+
await relinkaAsync("info", "Async message");
|
|
97
|
+
await relinkaAsync("success", "Operation completed");
|
|
49
98
|
```
|
|
50
99
|
|
|
51
|
-
|
|
100
|
+
#### `relinkaConfig(options?)`
|
|
101
|
+
|
|
102
|
+
Load configuration with optional fresh log file support.
|
|
52
103
|
|
|
53
104
|
```ts
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
relinkaAsync,
|
|
57
|
-
relinkaConfig,
|
|
58
|
-
relinkaShutdown,
|
|
59
|
-
} from "@reliverse/relinka";
|
|
60
|
-
|
|
61
|
-
export async function main() {
|
|
62
|
-
await relinkaAsync(
|
|
63
|
-
// this automatically loads the config
|
|
64
|
-
"verbose",
|
|
65
|
-
"This ASYNC verbose message can be seen only if verbose=true (in user config)",
|
|
66
|
-
);
|
|
67
|
-
await relinkaConfig; // place this at your main function or just at the top of your entry file
|
|
68
|
-
relinka(
|
|
69
|
-
"verbose",
|
|
70
|
-
"This SYNC verbose message can be seen only if verbose=true (in user config) AND config was loaded ",
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// --- BOX LEVEL EXAMPLES ---
|
|
74
|
-
relinka("box", "This is a boxed message using direct syntax!");
|
|
75
|
-
relinka.box("This is a boxed message using method syntax!");
|
|
76
|
-
|
|
77
|
-
// --- MESSAGE LEVEL EXAMPLES ---
|
|
78
|
-
relinka("message", "This is a message using direct syntax!");
|
|
79
|
-
relinka.message("This is a message using method syntax!");
|
|
80
|
-
|
|
81
|
-
// --- STEP LEVEL EXAMPLES ---
|
|
82
|
-
relinka("step", "Step 1: Initialize application");
|
|
83
|
-
relinka.step("Step 2: Load configuration");
|
|
84
|
-
relinka.step("Step 3: Start services");
|
|
85
|
-
|
|
86
|
-
// --- LOG LEVEL EXAMPLES ---
|
|
87
|
-
relinka("log", "Hello! 👋");
|
|
88
|
-
relinka("log", "Great to see you here!");
|
|
89
|
-
relinka("info", "Everything is running smoothly");
|
|
90
|
-
relinka("warn", "This might be a problem");
|
|
91
|
-
relinka(
|
|
92
|
-
"error", // non-fatal issue level can be recovered
|
|
93
|
-
"Uh oh, something broke",
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
relinka(
|
|
97
|
-
"null",
|
|
98
|
-
"'null' level has a special handling case: no symbol or spacing",
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// relinka(
|
|
102
|
-
// "fatal",
|
|
103
|
-
// "We should never reach this code! This should never happen! (see <anonymous> line)",
|
|
104
|
-
// ); // fatal level throws error and halts execution
|
|
105
|
-
relinka("success", "Thanks for using Relinka!");
|
|
106
|
-
|
|
107
|
-
// Make sure to shut down the logger at the end of your program
|
|
108
|
-
// This is important to flush all buffers and close file handles
|
|
109
|
-
await relinkaShutdown();
|
|
105
|
+
// Basic config loading
|
|
106
|
+
await relinkaConfig();
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
108
|
+
// With fresh log file (clears existing log file)
|
|
109
|
+
await relinkaConfig({ supportFreshLogFile: true });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### `relinkaShutdown()`
|
|
117
113
|
|
|
118
|
-
|
|
114
|
+
Flush all buffers and clean up resources. **Always call this at the end of your program.**
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
await relinkaShutdown();
|
|
119
118
|
```
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
### Utility Functions
|
|
121
|
+
|
|
122
|
+
#### `log`, `logger`
|
|
123
|
+
|
|
124
|
+
Aliases for the main `relinka` function.
|
|
122
125
|
|
|
123
126
|
```ts
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
import { log, logger } from "@reliverse/relinka";
|
|
128
|
+
|
|
129
|
+
log("info", "Using alias");
|
|
130
|
+
logger.success("Another alias");
|
|
126
131
|
```
|
|
127
132
|
|
|
128
|
-
####
|
|
133
|
+
#### `message(msg, ...args)`
|
|
134
|
+
|
|
135
|
+
Convenience function for general messages.
|
|
129
136
|
|
|
130
137
|
```ts
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
import { message } from "@reliverse/relinka";
|
|
139
|
+
message("This is a general message");
|
|
133
140
|
```
|
|
134
141
|
|
|
135
|
-
|
|
142
|
+
#### `step(msg, ...args)`
|
|
143
|
+
|
|
144
|
+
Convenience function for progress steps.
|
|
136
145
|
|
|
137
146
|
```ts
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
relinka("info", "");
|
|
142
|
-
// Async variant:
|
|
143
|
-
import { relinkaAsync } from "@reliverse/relinka";
|
|
144
|
-
await relinkaAsync("info", "Logged from async context");
|
|
145
|
-
// Coming soon:
|
|
146
|
-
await relinkaAsync("info", "Something happened", { animate: true });
|
|
147
|
+
import { step } from "@reliverse/relinka";
|
|
148
|
+
step("Step 1: Initialize");
|
|
149
|
+
step("Step 2: Load config");
|
|
147
150
|
```
|
|
148
151
|
|
|
149
|
-
##
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
Create a configuration file to customize Relinka's behavior:
|
|
155
|
+
|
|
156
|
+
### File Locations (in order of priority)
|
|
150
157
|
|
|
151
|
-
|
|
158
|
+
1. `relinka.config.ts` (project root) - **highest priority**
|
|
159
|
+
2. `.config/relinka.ts` - **lower priority**
|
|
160
|
+
|
|
161
|
+
### Configuration Example
|
|
152
162
|
|
|
153
163
|
```ts
|
|
154
164
|
import { defineConfig } from "@reliverse/relinka";
|
|
155
|
-
|
|
156
|
-
* RELINKA CONFIGURATION FILE
|
|
157
|
-
* - Hover over a field to see the information
|
|
158
|
-
* - Use intellisense to see available options
|
|
159
|
-
* @see https://github.com/reliverse/relinka
|
|
160
|
-
*/
|
|
165
|
+
|
|
161
166
|
export default defineConfig({
|
|
162
|
-
// Enable
|
|
167
|
+
// Enable verbose logging
|
|
163
168
|
verbose: false,
|
|
164
169
|
|
|
165
170
|
// Timestamp configuration
|
|
166
171
|
timestamp: {
|
|
167
|
-
enabled:
|
|
168
|
-
format: "HH:mm:ss",
|
|
172
|
+
enabled: true,
|
|
173
|
+
format: "YYYY-MM-DD HH:mm:ss.SSS",
|
|
169
174
|
},
|
|
170
175
|
|
|
171
|
-
//
|
|
176
|
+
// File logging
|
|
172
177
|
saveLogsToFile: true,
|
|
178
|
+
logFile: {
|
|
179
|
+
outputPath: "logs/app.log",
|
|
180
|
+
nameWithDate: "append-before", // "disable" | "append-before" | "append-after"
|
|
181
|
+
freshLogFile: true, // Clear log file on each run
|
|
182
|
+
},
|
|
173
183
|
|
|
174
|
-
//
|
|
175
|
-
|
|
184
|
+
// Log rotation
|
|
185
|
+
dirs: {
|
|
186
|
+
maxLogFiles: 10, // Keep only the 10 most recent log files
|
|
187
|
+
},
|
|
176
188
|
|
|
177
|
-
//
|
|
178
|
-
|
|
189
|
+
// Performance tuning
|
|
190
|
+
bufferSize: 4096, // 4KB buffer before flushing
|
|
191
|
+
maxBufferAge: 5000, // 5 seconds max buffer age
|
|
192
|
+
cleanupInterval: 10000, // 10 seconds between cleanups
|
|
179
193
|
|
|
194
|
+
// Customize log levels
|
|
180
195
|
levels: {
|
|
181
196
|
success: {
|
|
182
197
|
symbol: "✓",
|
|
@@ -185,138 +200,208 @@ export default defineConfig({
|
|
|
185
200
|
spacing: 3,
|
|
186
201
|
},
|
|
187
202
|
info: {
|
|
188
|
-
symbol: "
|
|
203
|
+
symbol: "◈",
|
|
189
204
|
fallbackSymbol: "[i]",
|
|
190
205
|
color: "cyanBright",
|
|
191
206
|
spacing: 3,
|
|
192
207
|
},
|
|
193
|
-
|
|
194
|
-
symbol: "✖",
|
|
195
|
-
fallbackSymbol: "[ERR]",
|
|
196
|
-
color: "redBright",
|
|
197
|
-
spacing: 3,
|
|
198
|
-
},
|
|
199
|
-
warn: {
|
|
200
|
-
symbol: "⚠",
|
|
201
|
-
fallbackSymbol: "[WARN]",
|
|
202
|
-
color: "yellowBright",
|
|
203
|
-
spacing: 3,
|
|
204
|
-
},
|
|
205
|
-
fatal: {
|
|
206
|
-
symbol: "‼",
|
|
207
|
-
fallbackSymbol: "[FATAL]",
|
|
208
|
-
color: "redBright",
|
|
209
|
-
spacing: 3,
|
|
210
|
-
},
|
|
211
|
-
verbose: {
|
|
212
|
-
symbol: "✧",
|
|
213
|
-
fallbackSymbol: "[VERBOSE]",
|
|
214
|
-
color: "gray",
|
|
215
|
-
spacing: 3,
|
|
216
|
-
},
|
|
217
|
-
log: { symbol: "│", fallbackSymbol: "|", color: "dim", spacing: 3 },
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
// Directory settings
|
|
221
|
-
dirs: {
|
|
222
|
-
dailyLogs: true,
|
|
223
|
-
logDir: "logs", // store logs in a custom folder
|
|
224
|
-
maxLogFiles: 5, // keep only the 5 most recent log files
|
|
225
|
-
specialDirs: {
|
|
226
|
-
distDirNames: [],
|
|
227
|
-
useParentConfigInDist: true,
|
|
228
|
-
},
|
|
208
|
+
// ... customize other levels
|
|
229
209
|
},
|
|
230
210
|
});
|
|
231
211
|
```
|
|
232
212
|
|
|
233
|
-
|
|
213
|
+
### Configuration Options
|
|
214
|
+
|
|
215
|
+
| Option | Type | Default | Description |
|
|
216
|
+
|--------|------|---------|-------------|
|
|
217
|
+
| `verbose` | `boolean` | `false` | Enable verbose logging |
|
|
218
|
+
| `saveLogsToFile` | `boolean` | `false` | Save logs to file |
|
|
219
|
+
| `disableColors` | `boolean` | `false` | Disable color output |
|
|
220
|
+
| `timestamp.enabled` | `boolean` | `false` | Add timestamps to logs |
|
|
221
|
+
| `timestamp.format` | `string` | `"YYYY-MM-DD HH:mm:ss.SSS"` | Timestamp format |
|
|
222
|
+
| `logFile.outputPath` | `string` | `"logs.log"` | Log file path |
|
|
223
|
+
| `logFile.nameWithDate` | `string` | `"disable"` | Date handling in filename |
|
|
224
|
+
| `logFile.freshLogFile` | `boolean` | `true` | Clear log file on startup |
|
|
225
|
+
| `dirs.maxLogFiles` | `number` | `0` | Maximum log files to keep |
|
|
226
|
+
| `bufferSize` | `number` | `4096` | Buffer size in bytes |
|
|
227
|
+
| `maxBufferAge` | `number` | `5000` | Max buffer age in ms |
|
|
228
|
+
| `cleanupInterval` | `number` | `10000` | Cleanup interval in ms |
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
- 🔜 other formats, supported by `reconf`, are coming soon
|
|
230
|
+
## File Logging
|
|
237
231
|
|
|
238
|
-
|
|
232
|
+
### File Naming Patterns
|
|
239
233
|
|
|
240
|
-
- Default
|
|
241
|
-
-
|
|
242
|
-
-
|
|
234
|
+
- **Default**: `logs.log`
|
|
235
|
+
- **With subdirectory**: `logs/app.log`
|
|
236
|
+
- **With date prefix**: `2025-01-15-logs.log`
|
|
237
|
+
- **With date suffix**: `logs-2025-01-15.log`
|
|
238
|
+
- **Combined**: `logs/2025-01-15-app.log`
|
|
243
239
|
|
|
244
|
-
|
|
240
|
+
### Automatic Cleanup
|
|
241
|
+
|
|
242
|
+
When `maxLogFiles` is set, Relinka automatically:
|
|
243
|
+
|
|
244
|
+
- Keeps only the N most recent log files
|
|
245
|
+
- Deletes older files during cleanup
|
|
246
|
+
- Runs cleanup periodically and on shutdown
|
|
247
|
+
|
|
248
|
+
## Advanced Usage
|
|
249
|
+
|
|
250
|
+
### Fatal Logging
|
|
251
|
+
|
|
252
|
+
Fatal logs throw errors and halt execution:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// This will throw an error and trigger debugger in development
|
|
256
|
+
relinka("fatal", "Critical system failure");
|
|
257
|
+
// or
|
|
258
|
+
relinka.fatal("Critical system failure");
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Box Formatting
|
|
262
|
+
|
|
263
|
+
Create visually appealing boxed messages:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
relinka("box", "This message will be displayed in a box");
|
|
267
|
+
relinka.box("This also works with method syntax");
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Async Context
|
|
271
|
+
|
|
272
|
+
Use `relinkaAsync` when you need to ensure configuration is loaded:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
await relinkaAsync("info", "This waits for config to load");
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Clear Terminal
|
|
279
|
+
|
|
280
|
+
Clear the terminal output:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
relinka("clear", "");
|
|
284
|
+
// or
|
|
285
|
+
relinka.clear();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Empty Lines
|
|
289
|
+
|
|
290
|
+
Add blank lines to output:
|
|
245
291
|
|
|
246
292
|
```ts
|
|
247
|
-
relinka("info", "
|
|
248
|
-
|
|
249
|
-
relinka("clear", ""); // clears terminal
|
|
293
|
+
relinka("info", "");
|
|
294
|
+
```
|
|
250
295
|
|
|
251
|
-
|
|
296
|
+
## Best Practices
|
|
297
|
+
|
|
298
|
+
### 1. Always Initialize and Shutdown
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
async function main() {
|
|
302
|
+
await relinkaConfig(); // At the start
|
|
303
|
+
|
|
304
|
+
// Your application logic
|
|
305
|
+
relinka("info", "App running");
|
|
306
|
+
|
|
307
|
+
await relinkaShutdown(); // At the end
|
|
308
|
+
}
|
|
252
309
|
```
|
|
253
310
|
|
|
311
|
+
### 2. Use Appropriate Log Levels
|
|
312
|
+
|
|
254
313
|
```ts
|
|
255
|
-
|
|
314
|
+
relinka("info", "User logged in"); // General info
|
|
315
|
+
relinka("success", "Payment processed"); // Success events
|
|
316
|
+
relinka("warn", "High memory usage"); // Warnings
|
|
317
|
+
relinka("error", "Database connection failed"); // Recoverable errors
|
|
318
|
+
relinka("fatal", "Critical system failure"); // Unrecoverable errors
|
|
256
319
|
```
|
|
257
320
|
|
|
258
|
-
|
|
321
|
+
### 3. Leverage Verbose Logging
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
relinka("verbose", "Debug information"); // Only shown when verbose=true
|
|
325
|
+
```
|
|
259
326
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
327
|
+
### 4. Structure Complex Data
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
relinka("error", "API request failed", {
|
|
331
|
+
endpoint: "/api/users",
|
|
332
|
+
statusCode: 500,
|
|
333
|
+
error: error.message
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Performance Features
|
|
338
|
+
|
|
339
|
+
- **Intelligent Buffering**: Logs are buffered and flushed efficiently
|
|
340
|
+
- **Async File Operations**: Non-blocking file writes
|
|
341
|
+
- **Memory Management**: Automatic cleanup of old log files
|
|
342
|
+
- **Unicode Detection**: Automatic fallback for non-Unicode terminals
|
|
265
343
|
|
|
266
344
|
## FAQ
|
|
267
345
|
|
|
268
|
-
### Why
|
|
346
|
+
### Why does my terminal hang after logging?
|
|
269
347
|
|
|
270
|
-
→ You
|
|
348
|
+
→ You forgot to call `await relinkaShutdown()` at the end of your program. This is required to flush buffers.
|
|
271
349
|
|
|
272
|
-
### Why my
|
|
350
|
+
### Why isn't my configuration loading?
|
|
273
351
|
|
|
274
|
-
→
|
|
352
|
+
→ Make sure you call `await relinkaConfig()` at the start of your application.
|
|
275
353
|
|
|
276
|
-
###
|
|
354
|
+
### Does `fatal` always throw?
|
|
277
355
|
|
|
278
|
-
→
|
|
356
|
+
→ Yes, `fatal` logs always throw errors and halt execution. In development mode, they also trigger the debugger.
|
|
279
357
|
|
|
280
|
-
###
|
|
358
|
+
### How do I disable colors?
|
|
281
359
|
|
|
282
|
-
→
|
|
360
|
+
→ Set `disableColors: true` in your configuration or use `NO_COLOR=1` environment variable.
|
|
283
361
|
|
|
284
|
-
###
|
|
362
|
+
### Can I use Relinka in both sync and async contexts?
|
|
285
363
|
|
|
286
|
-
|
|
287
|
-
- Use `relinka(level, message, ...args)` (recommended).
|
|
288
|
-
- 🔜 Or, just `relinka.level(message, ...args)`
|
|
289
|
-
- 🔜 Both designed to work with both sync (default) and async/await.
|
|
290
|
-
- 🔜 Both designed to work with both direct and wrapper methods.
|
|
291
|
-
- 🔜 Use the async logger if you want some advanced features (like typing text streaming animation).
|
|
364
|
+
→ Yes! Use `relinka()` for sync operations and `relinkaAsync()` when you need to use some advanced features (like typing text streaming animation).
|
|
292
365
|
|
|
293
|
-
|
|
366
|
+
### What's the difference between `log` and `message` levels?
|
|
294
367
|
|
|
295
|
-
|
|
296
|
-
- Want type-safe injections? Try [`@reliverse/reinject`](https://npmjs.com/@reliverse/reinject)
|
|
297
|
-
- For advanced bundling? Pair with [`@reliverse/dler`](https://npmjs.com/@reliverse/dler)
|
|
368
|
+
→ `log` uses a pipe symbol (│) and is for general logging, while `message` uses a different symbol (🞠) and is for general messages.
|
|
298
369
|
|
|
299
|
-
##
|
|
370
|
+
## Ecosystem
|
|
300
371
|
|
|
301
|
-
|
|
302
|
-
- [x] Timestamps
|
|
303
|
-
- [x] Daily logs
|
|
304
|
-
- [x] Log rotation
|
|
305
|
-
- [x] `fatal` type
|
|
306
|
-
- [x] Runtime config
|
|
307
|
-
- [ ] Implement per-project config redefinition
|
|
308
|
-
- [ ] Plugin support (custom formatters, hooks)
|
|
309
|
-
- [ ] CLI interface (to manage logs, config, etc)
|
|
372
|
+
Relinka works great with other Reliverse tools:
|
|
310
373
|
|
|
311
|
-
|
|
374
|
+
- **CLI Development**: [`@reliverse/prompts`](https://npmjs.com/@reliverse/prompts)
|
|
375
|
+
- **Bundling**: [`@reliverse/dler`](https://npmjs.com/@reliverse/dler)
|
|
312
376
|
|
|
313
|
-
|
|
377
|
+
## Roadmap
|
|
378
|
+
|
|
379
|
+
- [x] File logging with rotation
|
|
380
|
+
- [x] Timestamp support
|
|
381
|
+
- [x] Date-based file naming
|
|
382
|
+
- [x] Automatic cleanup
|
|
383
|
+
- [x] Fatal logging with debugger
|
|
384
|
+
- [x] Runtime configuration
|
|
385
|
+
- [x] Dual syntax support
|
|
386
|
+
- [ ] Plugin system
|
|
387
|
+
- [ ] Custom formatters
|
|
388
|
+
- [ ] CLI interface for log management
|
|
389
|
+
- [ ] WebSocket streaming
|
|
390
|
+
- [ ] Structured logging (JSON)
|
|
391
|
+
|
|
392
|
+
## Contributing
|
|
314
393
|
|
|
315
|
-
|
|
316
|
-
- [winston](https://github.com/winstonjs/winston#readme)
|
|
317
|
-
- [pino](https://github.com/pinojs/pino#readme)
|
|
318
|
-
- [node-bunyan](https://github.com/trentm/node-bunyan#readme)
|
|
394
|
+
We welcome contributions! Please see our [contributing guide](CONTRIBUTING.md) for details.
|
|
319
395
|
|
|
320
396
|
## License
|
|
321
397
|
|
|
322
398
|
💖 MIT © 2025 [blefnk Nazar Kornienko](https://github.com/blefnk)
|
|
399
|
+
|
|
400
|
+
## Acknowledgments
|
|
401
|
+
|
|
402
|
+
Relinka is inspired by these excellent logging libraries:
|
|
403
|
+
|
|
404
|
+
- [unjs/consola](https://github.com/unjs/consola) - Console logging for Node.js
|
|
405
|
+
- [winston](https://github.com/winstonjs/winston) - Multi-transport logging
|
|
406
|
+
- [pino](https://github.com/pinojs/pino) - Fast Node.js logger
|
|
407
|
+
- [node-bunyan](https://github.com/trentm/node-bunyan) - JSON logging
|
package/bin/impl.d.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import type { DefaultColorKeys } from "@reliverse/relico";
|
|
2
|
-
/** Configuration for special directory handling. */
|
|
3
|
-
export type RelinkaSpecialDirsConfig = {
|
|
4
|
-
distDirNames?: string[];
|
|
5
|
-
useParentConfigInDist?: boolean;
|
|
6
|
-
};
|
|
7
2
|
/** Configuration for directory-related settings. */
|
|
8
3
|
export type RelinkaDirsConfig = {
|
|
9
|
-
dailyLogs?: boolean;
|
|
10
|
-
logDir?: string;
|
|
11
4
|
maxLogFiles?: number;
|
|
12
|
-
specialDirs?: RelinkaSpecialDirsConfig;
|
|
13
5
|
};
|
|
14
6
|
/** Configuration for a single log level. */
|
|
15
7
|
export type LogLevelConfig = {
|
|
@@ -50,12 +42,7 @@ export type RelinkaConfig = {
|
|
|
50
42
|
verbose?: boolean;
|
|
51
43
|
/**
|
|
52
44
|
* Configuration for directory-related settings.
|
|
53
|
-
* - `dailyLogs`: If true, logs will be stored in a daily subdirectory.
|
|
54
|
-
* - `logDir`: The base directory for logs.
|
|
55
45
|
* - `maxLogFiles`: The maximum number of log files to keep before cleanup.
|
|
56
|
-
* - `specialDirs`: Configuration for special directory handling.
|
|
57
|
-
* - `distDirNames`: An array of directory names to check for special handling.
|
|
58
|
-
* - `useParentConfigInDist`: If true, use the parent config in dist directories.
|
|
59
46
|
*/
|
|
60
47
|
dirs?: RelinkaDirsConfig;
|
|
61
48
|
/**
|
|
@@ -63,9 +50,26 @@ export type RelinkaConfig = {
|
|
|
63
50
|
*/
|
|
64
51
|
disableColors?: boolean;
|
|
65
52
|
/**
|
|
66
|
-
*
|
|
53
|
+
* Configuration for log file output.
|
|
67
54
|
*/
|
|
68
|
-
|
|
55
|
+
logFile?: {
|
|
56
|
+
/**
|
|
57
|
+
* Path to the log file.
|
|
58
|
+
*/
|
|
59
|
+
outputPath?: string;
|
|
60
|
+
/**
|
|
61
|
+
* How to handle date in the filename.
|
|
62
|
+
* - `disable`: No date prefix/suffix
|
|
63
|
+
* - `append-before`: Add date before the filename (e.g., "2024-01-15-log.txt")
|
|
64
|
+
* - `append-after`: Add date after the filename (e.g., "log-2024-01-15.txt")
|
|
65
|
+
*/
|
|
66
|
+
nameWithDate?: "disable" | "append-before" | "append-after";
|
|
67
|
+
/**
|
|
68
|
+
* If true, clears the log file when relinkaConfig is executed with supportFreshLogFile: true.
|
|
69
|
+
* This is useful for starting with a clean log file on each run.
|
|
70
|
+
*/
|
|
71
|
+
freshLogFile?: boolean;
|
|
72
|
+
};
|
|
69
73
|
/**
|
|
70
74
|
* If true, logs will be saved to a file.
|
|
71
75
|
*/
|
|
@@ -108,8 +112,16 @@ export type LogFileInfo = {
|
|
|
108
112
|
path: string;
|
|
109
113
|
mtime: number;
|
|
110
114
|
};
|
|
115
|
+
/** Options for relinkaConfig */
|
|
116
|
+
export type RelinkaConfigOptions = {
|
|
117
|
+
supportFreshLogFile?: boolean;
|
|
118
|
+
};
|
|
111
119
|
/** Promise resolved once the user's config (if any) is merged. */
|
|
112
|
-
export declare const
|
|
120
|
+
export declare const loadRelinkaConfig: Promise<RelinkaConfig>;
|
|
121
|
+
/**
|
|
122
|
+
* Enhanced relinkaConfig function that accepts options
|
|
123
|
+
*/
|
|
124
|
+
export declare function relinkaConfig(options?: RelinkaConfigOptions): Promise<RelinkaConfig>;
|
|
113
125
|
/**
|
|
114
126
|
* Shuts down the logger, flushing all buffers and clearing timers.
|
|
115
127
|
* As Relinka user - call this at the end of your program to prevent hanging.
|
package/bin/impl.js
CHANGED
|
@@ -8,16 +8,14 @@ const EXIT_GUARD = Symbol.for("relinka.exitHandlersRegistered");
|
|
|
8
8
|
const DEFAULT_RELINKA_CONFIG = {
|
|
9
9
|
verbose: false,
|
|
10
10
|
dirs: {
|
|
11
|
-
|
|
12
|
-
logDir: "logs",
|
|
13
|
-
maxLogFiles: 0,
|
|
14
|
-
specialDirs: {
|
|
15
|
-
distDirNames: ["dist", "dist-jsr", "dist-npm", "dist-libs"],
|
|
16
|
-
useParentConfigInDist: true
|
|
17
|
-
}
|
|
11
|
+
maxLogFiles: 0
|
|
18
12
|
},
|
|
19
13
|
disableColors: false,
|
|
20
|
-
|
|
14
|
+
logFile: {
|
|
15
|
+
outputPath: "logs.log",
|
|
16
|
+
nameWithDate: "disable",
|
|
17
|
+
freshLogFile: true
|
|
18
|
+
},
|
|
21
19
|
saveLogsToFile: false,
|
|
22
20
|
timestamp: {
|
|
23
21
|
enabled: false,
|
|
@@ -115,9 +113,33 @@ function isUnicodeSupported() {
|
|
|
115
113
|
let currentConfig = { ...DEFAULT_RELINKA_CONFIG };
|
|
116
114
|
let isConfigInitialized = false;
|
|
117
115
|
let resolveRelinkaConfig;
|
|
118
|
-
|
|
116
|
+
let userTerminalCwd;
|
|
117
|
+
export const loadRelinkaConfig = new Promise((res) => {
|
|
119
118
|
resolveRelinkaConfig = res;
|
|
120
119
|
});
|
|
120
|
+
export async function relinkaConfig(options = {}) {
|
|
121
|
+
if (!userTerminalCwd) {
|
|
122
|
+
userTerminalCwd = process.cwd();
|
|
123
|
+
}
|
|
124
|
+
const config = await loadRelinkaConfig;
|
|
125
|
+
if (options.supportFreshLogFile && isFreshLogFileEnabled(config)) {
|
|
126
|
+
try {
|
|
127
|
+
const logFilePath = getLogFilePath(config);
|
|
128
|
+
await fs.ensureDir(path.dirname(logFilePath));
|
|
129
|
+
await fs.writeFile(logFilePath, "");
|
|
130
|
+
if (isVerboseEnabled(config)) {
|
|
131
|
+
console.log(`[Relinka Fresh Log] Cleared log file: ${logFilePath}`);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (isVerboseEnabled(config)) {
|
|
135
|
+
console.error(
|
|
136
|
+
`[Relinka Fresh Log Error] Failed to clear log file: ${err instanceof Error ? err.message : String(err)}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
121
143
|
const logBuffers = /* @__PURE__ */ new Map();
|
|
122
144
|
const activeTimers = [];
|
|
123
145
|
let bufferFlushTimer = null;
|
|
@@ -193,20 +215,15 @@ function isVerboseEnabled(config) {
|
|
|
193
215
|
function isColorEnabled(config) {
|
|
194
216
|
return !(config.disableColors ?? DEFAULT_RELINKA_CONFIG.disableColors);
|
|
195
217
|
}
|
|
196
|
-
function getLogDir(config) {
|
|
197
|
-
return config.dirs?.logDir ?? DEFAULT_RELINKA_CONFIG.dirs?.logDir ?? "logs";
|
|
198
|
-
}
|
|
199
|
-
function isDailyLogsEnabled(config) {
|
|
200
|
-
return config.dirs?.dailyLogs ?? DEFAULT_RELINKA_CONFIG.dirs?.dailyLogs ?? false;
|
|
201
|
-
}
|
|
202
218
|
function shouldSaveLogs(config) {
|
|
203
219
|
return config.saveLogsToFile ?? DEFAULT_RELINKA_CONFIG.saveLogsToFile ?? false;
|
|
204
220
|
}
|
|
205
221
|
function getMaxLogFiles(config) {
|
|
206
|
-
return config.dirs?.maxLogFiles ??
|
|
222
|
+
return config.dirs?.maxLogFiles ?? 0;
|
|
207
223
|
}
|
|
208
224
|
function getBaseLogName(config) {
|
|
209
|
-
|
|
225
|
+
const logFileConfig = config.logFile || DEFAULT_RELINKA_CONFIG.logFile || {};
|
|
226
|
+
return logFileConfig.outputPath ?? "logs.log";
|
|
210
227
|
}
|
|
211
228
|
function getBufferSize(config) {
|
|
212
229
|
return config.bufferSize ?? DEFAULT_RELINKA_CONFIG.bufferSize ?? 4096;
|
|
@@ -217,6 +234,9 @@ function getMaxBufferAge(config) {
|
|
|
217
234
|
function getCleanupInterval(config) {
|
|
218
235
|
return config.cleanupInterval ?? DEFAULT_RELINKA_CONFIG.cleanupInterval ?? 1e4;
|
|
219
236
|
}
|
|
237
|
+
function isFreshLogFileEnabled(config) {
|
|
238
|
+
return config.logFile?.freshLogFile ?? DEFAULT_RELINKA_CONFIG.logFile?.freshLogFile ?? false;
|
|
239
|
+
}
|
|
220
240
|
function isDevEnv() {
|
|
221
241
|
return process.env.NODE_ENV === "development";
|
|
222
242
|
}
|
|
@@ -232,18 +252,27 @@ function getTimestamp(config) {
|
|
|
232
252
|
return format.replace("YYYY", String(now.getFullYear())).replace("MM", String(now.getMonth() + 1).padStart(2, "0")).replace("DD", String(now.getDate()).padStart(2, "0")).replace("HH", String(now.getHours()).padStart(2, "0")).replace("mm", String(now.getMinutes()).padStart(2, "0")).replace("ss", String(now.getSeconds()).padStart(2, "0")).replace("SSS", String(now.getMilliseconds()).padStart(3, "0"));
|
|
233
253
|
}
|
|
234
254
|
function getLogFilePath(config) {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
255
|
+
const logFileConfig = config.logFile || DEFAULT_RELINKA_CONFIG.logFile || {};
|
|
256
|
+
const nameWithDate = logFileConfig.nameWithDate || "disable";
|
|
257
|
+
const outputPath = getBaseLogName(config);
|
|
258
|
+
const dir = path.dirname(outputPath);
|
|
259
|
+
let filename = path.basename(outputPath);
|
|
260
|
+
if (nameWithDate !== "disable") {
|
|
261
|
+
const dateString = getDateString();
|
|
262
|
+
if (nameWithDate === "append-before") {
|
|
263
|
+
filename = `${dateString}-${filename}`;
|
|
264
|
+
} else if (nameWithDate === "append-after") {
|
|
265
|
+
const nameWithoutExt = filename.replace(/\.log$/, "");
|
|
266
|
+
filename = `${nameWithoutExt}-${dateString}.log`;
|
|
267
|
+
}
|
|
241
268
|
}
|
|
242
|
-
if (
|
|
243
|
-
|
|
269
|
+
if (filename && !filename.endsWith(".log")) {
|
|
270
|
+
filename += ".log";
|
|
244
271
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
272
|
+
const effectiveFilename = filename || "logs.log";
|
|
273
|
+
const finalPath = dir === "." ? effectiveFilename : path.join(dir, effectiveFilename);
|
|
274
|
+
const baseCwd = userTerminalCwd || process.cwd();
|
|
275
|
+
return path.resolve(baseCwd, finalPath);
|
|
247
276
|
}
|
|
248
277
|
function getLevelStyle(config, level) {
|
|
249
278
|
const allLevels = config.levels || DEFAULT_RELINKA_CONFIG.levels || {};
|
|
@@ -305,6 +334,7 @@ const consoleMethodMap = {
|
|
|
305
334
|
success: console.log,
|
|
306
335
|
verbose: console.log,
|
|
307
336
|
log: console.log,
|
|
337
|
+
internal: console.log,
|
|
308
338
|
step: console.log,
|
|
309
339
|
box: console.log,
|
|
310
340
|
message: console.log,
|
|
@@ -322,16 +352,40 @@ function logToConsole(config, level, formattedMessage) {
|
|
|
322
352
|
method(`${colorFn(formattedMessage)}\x1B[0m`);
|
|
323
353
|
}
|
|
324
354
|
async function getLogFilesSortedByDate(config) {
|
|
325
|
-
const logDirectoryPath =
|
|
355
|
+
const logDirectoryPath = userTerminalCwd || process.cwd();
|
|
326
356
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
357
|
+
const files = await fs.readdir(logDirectoryPath);
|
|
358
|
+
const logFiles = [];
|
|
359
|
+
for (const file of files) {
|
|
360
|
+
const filePath = path.join(logDirectoryPath, file);
|
|
361
|
+
try {
|
|
362
|
+
const stats = await fs.stat(filePath);
|
|
363
|
+
if (stats.isFile() && file.endsWith(".log")) {
|
|
364
|
+
logFiles.push(file);
|
|
365
|
+
} else if (stats.isDirectory()) {
|
|
366
|
+
try {
|
|
367
|
+
const subFiles = await fs.readdir(filePath);
|
|
368
|
+
for (const subFile of subFiles) {
|
|
369
|
+
if (subFile.endsWith(".log")) {
|
|
370
|
+
logFiles.push(path.join(file, subFile));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch (subDirErr) {
|
|
374
|
+
if (isVerboseEnabled(config)) {
|
|
375
|
+
console.error(
|
|
376
|
+
`[Relinka FS Debug] Error reading subdirectory ${filePath}: ${subDirErr instanceof Error ? subDirErr.message : String(subDirErr)}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
if (isVerboseEnabled(config)) {
|
|
383
|
+
console.error(
|
|
384
|
+
`[Relinka FS Debug] Error accessing ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
330
387
|
}
|
|
331
|
-
return [];
|
|
332
388
|
}
|
|
333
|
-
const files = await fs.readdir(logDirectoryPath);
|
|
334
|
-
const logFiles = files.filter((f) => f.endsWith(".log"));
|
|
335
389
|
if (logFiles.length === 0) return [];
|
|
336
390
|
const fileInfoPromises = logFiles.map(
|
|
337
391
|
async (fileName) => {
|
|
@@ -357,7 +411,7 @@ async function getLogFilesSortedByDate(config) {
|
|
|
357
411
|
} catch (readErr) {
|
|
358
412
|
if (isVerboseEnabled(config)) {
|
|
359
413
|
console.error(
|
|
360
|
-
`[Relinka FS Error] Failed reading
|
|
414
|
+
`[Relinka FS Error] Failed reading directory ${logDirectoryPath}: ${readErr instanceof Error ? readErr.message : String(readErr)}`
|
|
361
415
|
);
|
|
362
416
|
}
|
|
363
417
|
return [];
|
|
@@ -615,7 +669,7 @@ export async function relinkaAsync(type, message, ...args) {
|
|
|
615
669
|
console.log();
|
|
616
670
|
return;
|
|
617
671
|
}
|
|
618
|
-
await relinkaConfig;
|
|
672
|
+
await relinkaConfig({ supportFreshLogFile: false });
|
|
619
673
|
const levelName = type.toLowerCase();
|
|
620
674
|
if (levelName === "fatal") {
|
|
621
675
|
shouldNeverHappen(message, ...args);
|
|
@@ -658,7 +712,7 @@ export function formatBox(text) {
|
|
|
658
712
|
const bottom = `\u2514${"\u2500".repeat(width)}\u2518`;
|
|
659
713
|
const content = lines.map((line) => {
|
|
660
714
|
const padding = width - line.length;
|
|
661
|
-
return `\u2502 ${line}${" ".repeat(padding)}\u2502`;
|
|
715
|
+
return `\u2502 ${line}${" ".repeat(padding - 2)}\u2502`;
|
|
662
716
|
}).join("\n");
|
|
663
717
|
return `${top}
|
|
664
718
|
${content}
|
package/bin/mod.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { message, step, log, logger } from "./alias.js";
|
|
2
|
-
export type {
|
|
3
|
-
export { relinkaConfig, relinkaShutdown, flushAllLogBuffers, shouldNeverHappen, truncateString, casesHandled, relinka, relinkaAsync, defineConfig, formatBox, } from "./impl.js";
|
|
2
|
+
export type { RelinkaDirsConfig, LogLevelConfig, LogLevelsConfig, LogLevel, RelinkaConfig, LogFileInfo, RelinkaFunction, RelinkaConfigOptions, } from "./impl.js";
|
|
3
|
+
export { loadRelinkaConfig, relinkaConfig, relinkaShutdown, flushAllLogBuffers, shouldNeverHappen, truncateString, casesHandled, relinka, relinkaAsync, defineConfig, formatBox, } from "./impl.js";
|
package/bin/mod.js
CHANGED