@stonyx/logs 1.0.1-alpha.2 → 1.0.1-alpha.20
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 +271 -57
- package/dist/color.d.ts +10 -0
- package/dist/color.js +39 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +299 -0
- package/package.json +22 -8
- package/src/color.js +0 -50
- package/src/index.js +0 -172
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://github.com/abofs/stonyx-logs/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/@stonyx/logs)
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
|
|
1
5
|
<h1 align="center">
|
|
2
6
|
<br>
|
|
3
7
|
<br>
|
|
@@ -27,10 +31,10 @@
|
|
|
27
31
|
|
|
28
32
|
---
|
|
29
33
|
|
|
30
|
-
**
|
|
31
|
-
This project is not directly associated with chalk other than chalk being a core dependency of **
|
|
34
|
+
**Log** is built on top of all the great work done by "Sindre Sorhus" and other collaborators of the [chalk](https://www.npmjs.com/package/chalk) project.
|
|
35
|
+
This project is not directly associated with chalk other than chalk being a core dependency of **Log**.
|
|
32
36
|
|
|
33
|
-
**IMPORTANT**: Please note that although **
|
|
37
|
+
**IMPORTANT**: Please note that although **Log** can be configured to any color through chalk, your output is subject to your terminal's color limitations.
|
|
34
38
|
|
|
35
39
|
## Highlights
|
|
36
40
|
|
|
@@ -41,27 +45,31 @@ This project is not directly associated with chalk other than chalk being a core
|
|
|
41
45
|
## Install
|
|
42
46
|
|
|
43
47
|
```sh
|
|
44
|
-
npm install
|
|
48
|
+
npm install @stonyx/logs
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
## Usage
|
|
48
52
|
|
|
49
53
|
```js
|
|
50
|
-
import
|
|
54
|
+
import Log from '@stonyx/logs';
|
|
55
|
+
|
|
56
|
+
const log = new Log();
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
log.info('Info: sample application has started');
|
|
59
|
+
log.warn('Warning: this is just a sample');
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
// logs to logs/error.log; a failed write rejects, and handling that rejection is required
|
|
62
|
+
// see "Handling write failures" below
|
|
63
|
+
log.error('Error: no application logic detected', true)
|
|
64
|
+
.catch(err => { /* err.code is the underlying fs error code, e.g. 'EACCES' */ });
|
|
57
65
|
```
|
|
58
66
|
|
|
59
67
|
Easily define your own logging mechanism and color-coding preference:
|
|
60
68
|
|
|
61
69
|
```js
|
|
62
|
-
import
|
|
70
|
+
import Log from '@stonyx/logs';
|
|
63
71
|
|
|
64
|
-
const
|
|
72
|
+
const log = new Log({
|
|
65
73
|
systemLogs: {
|
|
66
74
|
blue: '#007cae', // indigo blue
|
|
67
75
|
yellow: '#ae8f00', // bright orange
|
|
@@ -69,17 +77,17 @@ const chronicle = new Chronicle({
|
|
|
69
77
|
},
|
|
70
78
|
});
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
log.blue('Info: using custom method blue, sample application has started');
|
|
81
|
+
log.yellow('Warning: using custom method yellow, this is just a sample');
|
|
82
|
+
log.red('Error: using custom method red, no application logic detected', false);
|
|
75
83
|
```
|
|
76
84
|
|
|
77
85
|
Customize logging options to best suit your project
|
|
78
86
|
|
|
79
87
|
```js
|
|
80
|
-
import
|
|
88
|
+
import Log from '@stonyx/logs';
|
|
81
89
|
|
|
82
|
-
const
|
|
90
|
+
const log = new Log({
|
|
83
91
|
logToFileByDefault: true,
|
|
84
92
|
logTimestamp: true,
|
|
85
93
|
path: 'custom-logs', // <project root>/custom-logs/*.log
|
|
@@ -87,23 +95,31 @@ const chronicle = new Chronicle({
|
|
|
87
95
|
suffix: '\n=============================================================== \n',
|
|
88
96
|
});
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
log.info('Info: sample application has started')
|
|
99
|
+
.catch(err => { /* see "Handling write failures" */ });
|
|
91
100
|
```
|
|
101
|
+
|
|
102
|
+
> **Caveat:** with `logToFileByDefault: true`, every log call *except* [`debug()`](#the-debug-method)
|
|
103
|
+
> becomes a file write, and therefore returns a promise that can reject. A failing log directory turns
|
|
104
|
+
> an unhandled `log.info()` into a process-terminating rejection. See
|
|
105
|
+
> [handling write failures](#handling-write-failures).
|
|
106
|
+
|
|
92
107
|

|
|
93
108
|
|
|
94
109
|
|
|
95
110
|
Add additional log types extending the default options of "info", "warn", "error" and "debug"
|
|
96
111
|
|
|
97
112
|
```js
|
|
98
|
-
import
|
|
113
|
+
import Log from '@stonyx/logs';
|
|
99
114
|
|
|
100
|
-
const
|
|
115
|
+
const log = new Log({ additionalLogs: { question: 'green' } });
|
|
101
116
|
|
|
102
117
|
// create additional log with direct chalk configuration
|
|
103
|
-
|
|
118
|
+
log.defineType('query', log.chalk().black.bgGreen);
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
log.question('What will a fully custom chalk color function look like?');
|
|
121
|
+
// second argument writes to file, so this can reject -- see "Handling write failures"
|
|
122
|
+
await log.query('This is what a custom chalk color setting looks like', true);
|
|
107
123
|
```
|
|
108
124
|

|
|
109
125
|
|
|
@@ -111,7 +127,7 @@ await chronicle.query('This is what a custom chalk color setting looks like', tr
|
|
|
111
127
|
|
|
112
128
|
### Defining Logs & Colors
|
|
113
129
|
|
|
114
|
-
By default, **
|
|
130
|
+
By default, **Log** is instantiated with the following options:
|
|
115
131
|
|
|
116
132
|
```js
|
|
117
133
|
additionalLogs: {},
|
|
@@ -122,10 +138,10 @@ By default, **Chronicle** is instantiated with the following options:
|
|
|
122
138
|
},
|
|
123
139
|
```
|
|
124
140
|
|
|
125
|
-
You can add to a new log/color setting by passing the `additionalLogs` option to the **
|
|
141
|
+
You can add to a new log/color setting by passing the `additionalLogs` option to the **Log** constructor. Any setting that already exists in `systemLogs` will be replaced, otherwise they will be added.
|
|
126
142
|
|
|
127
143
|
```js
|
|
128
|
-
const
|
|
144
|
+
const log = new Log({ additionalLogs: { info: 'green', custom: 'cyan' } });
|
|
129
145
|
|
|
130
146
|
// output configuration:
|
|
131
147
|
{
|
|
@@ -136,32 +152,39 @@ You can add to a new log/color setting by passing the `additionalLogs` option to
|
|
|
136
152
|
}
|
|
137
153
|
```
|
|
138
154
|
|
|
139
|
-
**
|
|
155
|
+
**Log** will generate convenience methods for all keys provided, with the corresponding color settings. The example above would create the following convenience methods, for logging:
|
|
140
156
|
|
|
141
157
|
```js
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
log.info() // green output
|
|
159
|
+
log.warn() // yellow output
|
|
160
|
+
log.error() // red output
|
|
161
|
+
log.custom() // cyan output
|
|
146
162
|
```
|
|
147
163
|
|
|
148
164
|
These methods can then be called in your application with [logging parameters](#logging-parameters).
|
|
149
165
|
|
|
150
166
|
Color settings are handled by determining whether your input is a color name or a hex value (prefixed with **#**). For example, passing `red` as a color setting will utilize `chalk.red`, while passing `#ff0000` would use `chalk.hex('#ff0000')` instead. A [list of available colors](https://github.com/chalk/chalk#colors) can be found in chalks' documentation.
|
|
151
167
|
|
|
152
|
-
Additionally, these methods return a promise when `logToFile` is true, allowing you use them with `await` in an async method, or append `then()
|
|
168
|
+
Additionally, these methods return a promise when `logToFile` is true, allowing you to use them with `await` in an async method, or to append `then()`, `catch()` or `finally()`.
|
|
169
|
+
|
|
170
|
+
When `logToFile` is true that promise **can reject**, and handling the rejection is **required** --
|
|
171
|
+
see [handling write failures](#handling-write-failures) for the full contract.
|
|
153
172
|
|
|
154
173
|
```js
|
|
155
174
|
async method() {
|
|
156
|
-
|
|
175
|
+
try {
|
|
176
|
+
await log.error('error message', true);
|
|
157
177
|
|
|
158
|
-
|
|
178
|
+
// do something after logs/error.log (default) is created
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// err.code is the underlying fs error code, e.g. 'EACCES'
|
|
181
|
+
}
|
|
159
182
|
}
|
|
160
183
|
```
|
|
161
184
|
|
|
162
185
|
### The Debug Method
|
|
163
186
|
|
|
164
|
-
**
|
|
187
|
+
**Log** allows for the `log.debug()` method to be overridden by a color setting. However, by default we do not define a color for debug and debug is handled differently. For console logging, all **debug** does is output the following:
|
|
165
188
|
|
|
166
189
|
```js
|
|
167
190
|
// For logging to console:
|
|
@@ -171,32 +194,189 @@ console.dir(content);
|
|
|
171
194
|
JSON.stringify(content, null, 2);
|
|
172
195
|
```
|
|
173
196
|
|
|
174
|
-
We believe that when wanting to output complicated objects or debug **typescript** applications, there are better methods than utilizing this **
|
|
197
|
+
We believe that when wanting to output complicated objects or debug **typescript** applications, there are better methods than utilizing this **Log** package. But for anyone who's fully incorporated **Log** into their project, this function offers some convenience.
|
|
198
|
+
|
|
199
|
+
`debug(content, logToFile)` writes to file on the same contract as every other log type -- a failed
|
|
200
|
+
write rejects with the underlying `fs` error and must be handled -- with one difference: `debug()`
|
|
201
|
+
ignores [`logToFileByDefault`](#configuration), so it writes to file only when you pass `true`.
|
|
202
|
+
See [handling write failures](#handling-write-failures).
|
|
175
203
|
|
|
176
204
|
### Logging Parameters
|
|
177
205
|
|
|
178
206
|
```js
|
|
179
|
-
|
|
207
|
+
log.error('error message', true, false); // content, logToFile, overwrite
|
|
180
208
|
```
|
|
181
209
|
|
|
182
210
|
| Parameter | Type | Default | Description |
|
|
183
211
|
| :---: | :---: | :---: | :--- |
|
|
184
212
|
| `content` | **String** | | Content of log that will output on your console. |
|
|
185
|
-
| `logToFile` | **Boolean** | *false* | Option to log content to file. |
|
|
213
|
+
| `logToFile` | **Boolean** | *false* | Option to log content to file. When true, the call returns a promise that rejects if the write fails -- see [handling write failures](#handling-write-failures). |
|
|
186
214
|
| `overwrite` | **Boolean** | *false <br> (true on debug())* | Option to overwrite log file, rather than append to it. This option is redundant if logToFile is false. |
|
|
187
215
|
|
|
188
216
|
**logToFile** will log to *<project-root>/logs* unless [configured](#configuration) differently during instantiation. <br>
|
|
217
|
+
|
|
218
|
+
### Handling Write Failures
|
|
219
|
+
|
|
220
|
+
Any call that writes to a file returns a promise that **rejects when the write fails**. That covers
|
|
221
|
+
`log.error(content, true)`, `log.debug(content, true)`, and every log call except `debug()` when
|
|
222
|
+
[`logToFileByDefault`](#configuration) is `true` -- `debug()` is a direct method that never consults
|
|
223
|
+
`logToFileByDefault`, so it only writes to file when you pass `true` explicitly.
|
|
224
|
+
|
|
225
|
+
That rejection is the only actionable failure signal, so every file-writing call must be awaited
|
|
226
|
+
inside `try`/`catch`, or have `.catch()` attached. The console line is written *before* the file
|
|
227
|
+
write is attempted, so a rejection means only the file copy was lost -- do not re-log the message in
|
|
228
|
+
your handler, or it prints twice.
|
|
229
|
+
|
|
230
|
+
> **A rejection is terminal. Do not retry it.**
|
|
231
|
+
>
|
|
232
|
+
> For the recoverable directory-level codes -- `ENOENT`, `EACCES`, `EPERM`, `EROFS` -- **Log** has
|
|
233
|
+
> already retried internally by the time the rejection reaches you (see
|
|
234
|
+
> [self-healing retry](#self-healing-retry) below), so that retry ran and failed too. Every other
|
|
235
|
+
> code -- `EISDIR`, `ENOTDIR` and `ENOSPC` among them -- is not itself retried, because a retry
|
|
236
|
+
> cannot help it. Either way a consumer-side retry layer races the internal self-heal or repeats a call that
|
|
237
|
+
> already cannot succeed, and only delays the disable or back-off that your error path exists to
|
|
238
|
+
> trigger.
|
|
239
|
+
>
|
|
240
|
+
> Treat the **first** rejection as the signal to disable file logging or back off, and do not attempt
|
|
241
|
+
> the write again straight away. If you do want file logging to come back without a restart, re-arm
|
|
242
|
+
> on a timer -- see [recovering after a latch](#recovering-after-a-latch).
|
|
243
|
+
|
|
244
|
+
#### What the Promise Rejects With
|
|
245
|
+
|
|
246
|
+
The rejection value is the underlying Node `fs` error - a `NodeJS.ErrnoException` passed through
|
|
247
|
+
unmodified, with `err.code`, `err.syscall`, `err.errno` and `err.path` all preserved.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
try {
|
|
251
|
+
await log.error('error message', true);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// err.code is the underlying fs error code, e.g. 'EACCES'
|
|
254
|
+
// err.syscall is the failing call, e.g. 'mkdir' or 'open'
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Codes you are most likely to see are `EACCES` and `EPERM` (no permission to create the log directory
|
|
259
|
+
or write the log file), `ENOENT` (the directory disappeared and could not be recreated) and `EROFS`
|
|
260
|
+
(read-only filesystem) - but any error the filesystem raises reaches the caller as-is.
|
|
261
|
+
|
|
262
|
+
**An unhandled rejection terminates your process** under Node's default `--unhandled-rejections=throw`.
|
|
263
|
+
A fire-and-forget `log.error('...', true)` pointed at an unwritable log directory exits the process
|
|
264
|
+
with `Error: EACCES: permission denied, mkdir '...'`.
|
|
265
|
+
|
|
266
|
+
The recommended consumer shape is a latch that trips on the first rejection:
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
let fileLoggingDisabled = false;
|
|
270
|
+
|
|
271
|
+
async function logError(message) {
|
|
272
|
+
// once file logging has failed, stay on the console
|
|
273
|
+
if (fileLoggingDisabled) return log.error(message);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
await log.error(message, true);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// the first rejection is terminal - disable, do not retry
|
|
279
|
+
fileLoggingDisabled = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
That latch is deliberately terminal for the lifetime of the process: nothing in it ever re-enables
|
|
285
|
+
file logging. Records are degraded rather than lost, because the console line is still written on
|
|
286
|
+
every call -- but a consumer using the file sink for audit or compliance should treat the first
|
|
287
|
+
rejection as an **alertable** event, or fail closed, rather than degrade silently.
|
|
288
|
+
|
|
289
|
+
#### Recovering After a Latch
|
|
290
|
+
|
|
291
|
+
If file logging must come back without a restart, re-arm the latch on a **timer**, never on the next
|
|
292
|
+
log call. Hold it closed for a fixed back-off window -- a minute or more, long enough that a
|
|
293
|
+
permissions fix or a freed disk has a chance to land -- then let exactly one write through. If that
|
|
294
|
+
write rejects, latch again and lengthen the window; if it succeeds, clear the latch. Anything faster
|
|
295
|
+
is a consumer-side retry under another name.
|
|
296
|
+
|
|
297
|
+
This also decides what your operators can see. `log-write-recovered` (see
|
|
298
|
+
[structured stderr notices](#structured-stderr-notices)) is emitted **only** on a subsequent
|
|
299
|
+
successful write, and a consumer that latches off permanently never issues one. Its entire stderr
|
|
300
|
+
output for the episode is a single `log-write-failed` followed by silence, which is indistinguishable
|
|
301
|
+
from a failure that self-healed -- so an alert-clear rule keyed on `log-write-recovered` will never
|
|
302
|
+
fire for it. Either treat `log-write-failed` as latching on the operator side too, or use the timed
|
|
303
|
+
re-arm above, which is what makes a recovery notice reachable at all.
|
|
304
|
+
|
|
305
|
+
#### Self-Healing Retry
|
|
306
|
+
|
|
307
|
+
A cached log directory can outlive the directory it describes - for example, something deletes
|
|
308
|
+
`logs/` while your process is running. To cover that, a failure whose `err.code` is `ENOENT`,
|
|
309
|
+
`EACCES`, `EPERM` or `EROFS` drops the cached directory entry and retries the write **exactly once**.
|
|
310
|
+
Any other code -- `EISDIR`, `ENOTDIR`, `ENOSPC`, `EMFILE` and the rest -- does not itself trigger
|
|
311
|
+
a retry. The decision is keyed on the *first* attempt's code, so a rejection carrying one of these
|
|
312
|
+
can still arrive after a retry if conditions changed mid-call.
|
|
313
|
+
|
|
314
|
+
A retry that succeeds emits no *failure* notice: the promise resolves normally and the write counts
|
|
315
|
+
as a success, closing any failure episode already open for that directory with the usual
|
|
316
|
+
`log-write-recovered`. A rejection carrying one of those four codes has therefore already spent its
|
|
317
|
+
single retry; every other code was never retryable in the first place. The retry is per call and per
|
|
318
|
+
`Log` instance -- independent instances retry independently.
|
|
319
|
+
|
|
320
|
+
#### Structured stderr Notices
|
|
321
|
+
|
|
322
|
+
Alongside the rejection, **Log** emits a machine-parseable notice to `stderr` so that operators have
|
|
323
|
+
something to key on. The write-failure path must never re-enter the logger itself, so these notices
|
|
324
|
+
bypass all formatting and configuration: they are emitted via `console.error` as one JSON object per
|
|
325
|
+
line (JSONL), regardless of your color, prefix, suffix or path settings. Anything that wraps or
|
|
326
|
+
patches `console.error` sees them too. Notices are informational; the promise rejection is the
|
|
327
|
+
failure signal.
|
|
328
|
+
|
|
329
|
+
| Field | Value |
|
|
330
|
+
| :---: | :--- |
|
|
331
|
+
| `ts` | ISO-8601 timestamp of the notice |
|
|
332
|
+
| `surface` | Always `"stonyx-logs"` |
|
|
333
|
+
| `sessionKey` | Always `null` |
|
|
334
|
+
| `project` | Always `null` |
|
|
335
|
+
| `severity` | `"error"` for a failure, `"warn"` for a recovery |
|
|
336
|
+
| `event` | `"log-write-failed"` or `"log-write-recovered"` |
|
|
337
|
+
| `payload` | Event-specific, see below |
|
|
338
|
+
| `schemaVersion` | Always `1` |
|
|
339
|
+
|
|
340
|
+
`log-write-failed` payload:
|
|
341
|
+
|
|
342
|
+
| Field | Description |
|
|
343
|
+
| :---: | :--- |
|
|
344
|
+
| `targetLog` | Resolved path of the log file that could not be written |
|
|
345
|
+
| `path` | Resolved log *directory*, with trailing separator - this is the dedupe key |
|
|
346
|
+
| `syscall` | The failing syscall, e.g. `"mkdir"` or `"open"` |
|
|
347
|
+
| `code` | The same `err.code` carried by the rejection |
|
|
348
|
+
| `cached` | Whether the directory was already cached when the write started |
|
|
349
|
+
| `suppressedCount` | Always `0` on a failure notice |
|
|
350
|
+
|
|
351
|
+
`log-write-recovered` carries `targetLog`, `path` and `suppressedCount` only.
|
|
352
|
+
|
|
353
|
+
**Dedupe:** notices are one per failure episode, keyed on the resolved directory and scoped to the
|
|
354
|
+
`Log` instance -- the failure counters are instance state, so two instances writing to the same
|
|
355
|
+
directory emit one notice each. The first failure for a directory emits `log-write-failed`; every
|
|
356
|
+
subsequent failure for that same directory is silent. The next successful write to it emits a single
|
|
357
|
+
`log-write-recovered` carrying `suppressedCount` - the number of notices that were suppressed, i.e.
|
|
358
|
+
`N - 1` for an episode of `N` consecutive failures.
|
|
359
|
+
Every failure still rejects its own promise; only the notices are deduped.
|
|
360
|
+
|
|
361
|
+
Five consecutive failures followed by one success emit exactly these two records:
|
|
362
|
+
|
|
363
|
+
```jsonl
|
|
364
|
+
{"ts":"2026-08-29T23:08:50.343Z","surface":"stonyx-logs","sessionKey":null,"project":null,"severity":"error","event":"log-write-failed","payload":{"targetLog":"/private/tmp/my-app/logs/error.log","path":"/private/tmp/my-app/logs/","syscall":"mkdir","code":"EACCES","cached":false,"suppressedCount":0},"schemaVersion":1}
|
|
365
|
+
{"ts":"2026-08-29T23:08:50.344Z","surface":"stonyx-logs","sessionKey":null,"project":null,"severity":"warn","event":"log-write-recovered","payload":{"targetLog":"/private/tmp/my-app/logs/error.log","path":"/private/tmp/my-app/logs/","suppressedCount":4},"schemaVersion":1}
|
|
366
|
+
```
|
|
367
|
+
|
|
189
368
|
### Configuration
|
|
190
369
|
|
|
191
|
-
When instantiating **
|
|
370
|
+
When instantiating **Log**, you can pass an object to customize your settings. Below is the default configuration:
|
|
192
371
|
|
|
193
372
|
```js
|
|
194
|
-
const
|
|
373
|
+
const log = new Log({
|
|
195
374
|
logToFileByDefault: false,
|
|
196
375
|
logTimestamp: false,
|
|
197
376
|
path: 'logs/',
|
|
198
377
|
prefix: '',
|
|
199
378
|
suffix: '',
|
|
379
|
+
filename: '',
|
|
200
380
|
additionalLogs: {},
|
|
201
381
|
systemLogs: {
|
|
202
382
|
info: 'cyan',
|
|
@@ -208,32 +388,33 @@ const chronicle = new Chronicle({
|
|
|
208
388
|
|
|
209
389
|
| Option | Type | Default | Description |
|
|
210
390
|
| :---: | :---: | :---: | :--- |
|
|
211
|
-
| `logToFileByDefault` | **Boolean** | *false* | Option to change default setting for `logToFile` parameter of logging functions. |
|
|
391
|
+
| `logToFileByDefault` | **Boolean** | *false* | Option to change default setting for `logToFile` parameter of logging functions. When true, every log call except `debug()` becomes a rejectable file write -- see [handling write failures](#handling-write-failures). |
|
|
212
392
|
| `logTimestamp` | **Boolean** | *false* | Option to include timestamp in console logging. Timestamps are automatically included in file logs. |
|
|
213
393
|
| `path` | **String** | *'logs/'* | Path in which to store log files. This setting is relative to your project's root directory. |
|
|
214
394
|
| `prefix` | **String** | *''* | Prefix string to prepend all log messages for all log types with the exception of *debug*. |
|
|
215
395
|
| `suffix` | **String** | *''* | Suffix string to tack on to all log messages for all log types with the exception of *debug*. |
|
|
396
|
+
| `filename` | **String** | *''* | Template for log file names with variable support. Defaults to `{type}.log` when empty. See [dynamic file names](#dynamic-file-names). |
|
|
216
397
|
| `additionalLogs` | **Object** | | Key value pair object containing log type to color setting for logs that will be merged with `systemLogs` |
|
|
217
|
-
| `systemLogs` | **Object** | | Key value pair object containing log type to color setting for main **
|
|
398
|
+
| `systemLogs` | **Object** | | Key value pair object containing log type to color setting for main **Log** logs available in application |
|
|
218
399
|
|
|
219
400
|
`additionalLogs` and `systemLogs` are explained with more detail in the [defining logs and colors](#defining-logs) section.
|
|
220
401
|
|
|
221
402
|
### Advanced Configuration
|
|
222
403
|
|
|
223
|
-
You may want to do more than just pick a basic color for your output. **chalk** offers a variety of different options, and can be configured via `defineType()`. **
|
|
404
|
+
You may want to do more than just pick a basic color for your output. **chalk** offers a variety of different options, and can be configured via `defineType()`. **Log** exposes the chalk instance via `chalk()` so that you don't have to import **chalk** directly into your project. Here is an example of how you can use this method to fully customize your log color setting:
|
|
224
405
|
|
|
225
406
|
```js
|
|
226
|
-
const
|
|
407
|
+
const log = new Log();
|
|
227
408
|
|
|
228
|
-
|
|
229
|
-
|
|
409
|
+
log.defineType('critical', log.chalk().bold.red);
|
|
410
|
+
log.critical('This is a critical error');
|
|
230
411
|
```
|
|
231
412
|
|
|
232
413
|
Additionally, any [configuration](#configuration) that can be set during instantiation, can also be applied exclusively to any given type by passing in a third **options** parameter.
|
|
233
414
|
|
|
234
415
|
```js
|
|
235
416
|
// params: type, setting, options
|
|
236
|
-
|
|
417
|
+
log.definetype('notice', '#c0c0c0', {
|
|
237
418
|
prefix: '--------------------------------------------------------------- \n',
|
|
238
419
|
suffix: '\n=============================================================== \n'
|
|
239
420
|
});
|
|
@@ -248,29 +429,62 @@ chronicle.definetype('notice', '#c0c0c0', {
|
|
|
248
429
|
|
|
249
430
|
|
|
250
431
|
```js
|
|
251
|
-
const
|
|
432
|
+
const log = new Log();
|
|
252
433
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
434
|
+
log.defineType('info', log.chalk().black.bgCyan);
|
|
435
|
+
log.defineType('critical', log.chalk().bold.red);
|
|
436
|
+
log.defineType('dialog', 'magentaBright');
|
|
437
|
+
log.definetype('notice', '#c0c0c0', {
|
|
257
438
|
prefix: '--------------------------------------------------------------- \n',
|
|
258
439
|
suffix: '\n=============================================================== \n'
|
|
259
440
|
});
|
|
260
441
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
442
|
+
log.info('This pre-existing log now has a cyan background and black foreground');
|
|
443
|
+
log.critical('This new log is bold and red');
|
|
444
|
+
log.dialog('This new dialog is bright magenta');
|
|
445
|
+
log.notice('This new log is the hex "#c0c0c0" share of gray');
|
|
265
446
|
```
|
|
266
447
|
|
|
267
448
|
`defineType()` can also be used as an alternative to populating the `additionalLogs` setting in the constructor, as if the setting doesn't already exist, it will then be created.
|
|
268
449
|
|
|
450
|
+
### Dynamic File Names
|
|
451
|
+
|
|
452
|
+
The `filename` option supports template variables that are resolved at write-time, allowing each log type to produce uniquely named files.
|
|
453
|
+
|
|
454
|
+
#### Supported Variables
|
|
455
|
+
|
|
456
|
+
| Variable | Resolves To | Example Output |
|
|
457
|
+
| :---: | :--- | :--- |
|
|
458
|
+
| `{date}` | Current date in YYYY-MM-DD format | `2026-04-04` |
|
|
459
|
+
| `{type}` | Log type name | `error` |
|
|
460
|
+
| `{pid}` | Current process ID | `12345` |
|
|
461
|
+
| `{hostname}` | Machine hostname | `my-server` |
|
|
462
|
+
|
|
463
|
+
#### Examples
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
// Per-type filename via defineType
|
|
467
|
+
log.defineType('error', 'red', { filename: 'error-{date}.log' });
|
|
468
|
+
// writes to: logs/error-2026-04-04.log
|
|
469
|
+
|
|
470
|
+
// Per-type filename with multiple variables
|
|
471
|
+
log.defineType('info', 'cyan', { filename: '{type}-{hostname}-{date}.log' });
|
|
472
|
+
// writes to: logs/info-my-server-2026-04-04.log
|
|
473
|
+
|
|
474
|
+
// Global filename template via constructor
|
|
475
|
+
const log = new Log({ filename: '{type}-{date}.log' });
|
|
476
|
+
// all types write to: logs/<type>-2026-04-04.log
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
When no `filename` is configured, the default behavior of `{type}.log` is preserved for full backward compatibility.
|
|
480
|
+
|
|
481
|
+
Path traversal characters (`..`, `/`, `\`) are automatically stripped from resolved file names for security.
|
|
482
|
+
|
|
269
483
|
## Origin
|
|
270
484
|
|
|
271
|
-
As a team of developers who are constantly working on side projects, we often litter our codebase with TODOs to refactor convenience utils such as **
|
|
485
|
+
As a team of developers who are constantly working on side projects, we often litter our codebase with TODOs to refactor convenience utils such as **@stonyx/logs** into classes of their own, or projects of their own. This usually turns into internal tech debt that never gets addressed. Furthermore, we also often find ourselves going the *copy -> paste -> modify* route of previously written useful logic, which saves us time in new projects, but not as much as it would if all we had to do was run an `npm install` instead.
|
|
272
486
|
|
|
273
|
-
With that in mind, we are proud to release **
|
|
487
|
+
With that in mind, we are proud to release **@stonyx/logs** as an open source package, in hopes others will find this just as useful as we do in their own projects.
|
|
274
488
|
|
|
275
489
|
## Maintainers
|
|
276
490
|
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export type ChalkColorFn = (text: string) => string;
|
|
3
|
+
export type ColorSetting = string | ChalkColorFn;
|
|
4
|
+
export default class Color {
|
|
5
|
+
types: Record<string, ChalkColorFn>;
|
|
6
|
+
getLogColor(type: string): ChalkColorFn;
|
|
7
|
+
getChalkInstance(): typeof chalk;
|
|
8
|
+
setLogColor(type: string, setting: ColorSetting): void;
|
|
9
|
+
settingToChalkColorFunction(setting: ColorSetting): ChalkColorFn;
|
|
10
|
+
}
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export default class Color {
|
|
3
|
+
types = {};
|
|
4
|
+
getLogColor(type) {
|
|
5
|
+
return this.types[type];
|
|
6
|
+
}
|
|
7
|
+
getChalkInstance() {
|
|
8
|
+
return chalk;
|
|
9
|
+
}
|
|
10
|
+
setLogColor(type, setting) {
|
|
11
|
+
const chalkColorFunction = this.settingToChalkColorFunction(setting);
|
|
12
|
+
this.types[type] = chalkColorFunction;
|
|
13
|
+
}
|
|
14
|
+
// retrieves chalk color function, and fully validates output
|
|
15
|
+
settingToChalkColorFunction(setting) {
|
|
16
|
+
const errorMessage = 'Invalid chalk color function. '
|
|
17
|
+
+ 'For help with color settings, see https://github.com/abofs/stonyx-logs#defining-logs--colors';
|
|
18
|
+
switch (typeof setting) {
|
|
19
|
+
case 'string':
|
|
20
|
+
const chalkColorFunction = (setting[0] === '#')
|
|
21
|
+
? chalk.hex(setting)
|
|
22
|
+
: chalk[setting];
|
|
23
|
+
if (!chalkColorFunction
|
|
24
|
+
|| typeof chalkColorFunction !== 'function'
|
|
25
|
+
|| typeof chalkColorFunction('') !== 'string') {
|
|
26
|
+
throw new Error(errorMessage);
|
|
27
|
+
}
|
|
28
|
+
return chalkColorFunction;
|
|
29
|
+
case 'function':
|
|
30
|
+
// validate that given function returns a string
|
|
31
|
+
if (typeof setting('') !== 'string') {
|
|
32
|
+
throw new Error(errorMessage);
|
|
33
|
+
}
|
|
34
|
+
return setting;
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(errorMessage);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import Color, { type ColorSetting } from './color.js';
|
|
2
|
+
export type Severity = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
3
|
+
export interface LogOptions {
|
|
4
|
+
logToFileByDefault: boolean;
|
|
5
|
+
logTimestamp: boolean;
|
|
6
|
+
path: string;
|
|
7
|
+
prefix: string;
|
|
8
|
+
suffix: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
additionalLogs: Record<string, ColorSetting>;
|
|
11
|
+
systemLogs: Record<string, ColorSetting>;
|
|
12
|
+
}
|
|
13
|
+
export default class Log {
|
|
14
|
+
options: LogOptions;
|
|
15
|
+
color: Color;
|
|
16
|
+
typeOptions: Record<string, Partial<LogOptions>>;
|
|
17
|
+
directoryCache: Map<string, Promise<void>>;
|
|
18
|
+
writeFailures: Map<string, number>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Logs `content` to the console in the `info` color, and to file when `logToFile` is true.
|
|
22
|
+
*
|
|
23
|
+
* **A failed file write rejects the returned promise, and that rejection is terminal - do not retry
|
|
24
|
+
* it.** `ENOENT`, `EACCES`, `EPERM` and `EROFS` have already been retried once internally by the
|
|
25
|
+
* time one reaches you; every other code (`EISDIR`, `ENOTDIR`, `ENOSPC`, `EMFILE`, ...) is never
|
|
26
|
+
* retried, because a retry cannot help it. Handle the first rejection by disabling file logging or
|
|
27
|
+
* backing off - never by writing again. The rejection value is the underlying
|
|
28
|
+
* `NodeJS.ErrnoException` with `err.code` preserved, and leaving it unhandled terminates the
|
|
29
|
+
* process.
|
|
30
|
+
*
|
|
31
|
+
* @see https://github.com/abofs/stonyx-logs#handling-write-failures
|
|
32
|
+
*/
|
|
33
|
+
info: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Logs `content` to the console in the `warn` color, and to file when `logToFile` is true.
|
|
36
|
+
*
|
|
37
|
+
* **A failed file write rejects the returned promise, and that rejection is terminal - do not retry
|
|
38
|
+
* it.** `ENOENT`, `EACCES`, `EPERM` and `EROFS` have already been retried once internally by the
|
|
39
|
+
* time one reaches you; every other code (`EISDIR`, `ENOTDIR`, `ENOSPC`, `EMFILE`, ...) is never
|
|
40
|
+
* retried, because a retry cannot help it. Handle the first rejection by disabling file logging or
|
|
41
|
+
* backing off - never by writing again. The rejection value is the underlying
|
|
42
|
+
* `NodeJS.ErrnoException` with `err.code` preserved, and leaving it unhandled terminates the
|
|
43
|
+
* process.
|
|
44
|
+
*
|
|
45
|
+
* @see https://github.com/abofs/stonyx-logs#handling-write-failures
|
|
46
|
+
*/
|
|
47
|
+
warn: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Logs `content` to the console in the `error` color, and to file when `logToFile` is true.
|
|
50
|
+
*
|
|
51
|
+
* **A failed file write rejects the returned promise, and that rejection is terminal - do not retry
|
|
52
|
+
* it.** `ENOENT`, `EACCES`, `EPERM` and `EROFS` have already been retried once internally by the
|
|
53
|
+
* time one reaches you; every other code (`EISDIR`, `ENOTDIR`, `ENOSPC`, `EMFILE`, ...) is never
|
|
54
|
+
* retried, because a retry cannot help it. Handle the first rejection by disabling file logging or
|
|
55
|
+
* backing off - never by writing again. The rejection value is the underlying
|
|
56
|
+
* `NodeJS.ErrnoException` with `err.code` preserved, and leaving it unhandled terminates the
|
|
57
|
+
* process.
|
|
58
|
+
*
|
|
59
|
+
* @see https://github.com/abofs/stonyx-logs#handling-write-failures
|
|
60
|
+
*/
|
|
61
|
+
error: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
62
|
+
constructor(options?: Partial<LogOptions>);
|
|
63
|
+
defineType(type: string, setting: ColorSetting, options?: Partial<LogOptions> | null): void;
|
|
64
|
+
createConvenienceMethod(type: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Validates params and applies configuration-based defaults for logging. Every convenience method
|
|
67
|
+
* created by `defineType` - including `info`, `warn` and `error` - routes through here. Note that
|
|
68
|
+
* `debug` does not: it is a direct method that never consults `logToFileByDefault`.
|
|
69
|
+
*
|
|
70
|
+
* **A failed file write rejects the returned promise, and that rejection is terminal - do not retry
|
|
71
|
+
* it.** `ENOENT`, `EACCES`, `EPERM` and `EROFS` have already been retried once internally by the
|
|
72
|
+
* time one reaches you; every other code (`EISDIR`, `ENOTDIR`, `ENOSPC`, `EMFILE`, ...) is never
|
|
73
|
+
* retried, because a retry cannot help it. Handle the first rejection by disabling file logging or
|
|
74
|
+
* backing off - never by writing again. The rejection value is the underlying
|
|
75
|
+
* `NodeJS.ErrnoException` with `err.code` preserved, and leaving it unhandled terminates the
|
|
76
|
+
* process.
|
|
77
|
+
*
|
|
78
|
+
* @see https://github.com/abofs/stonyx-logs#handling-write-failures
|
|
79
|
+
*/
|
|
80
|
+
logAction(type: string, content: string, logToFile?: boolean, overwrite?: boolean): Promise<void>;
|
|
81
|
+
getOptionForType(type: string, option: keyof LogOptions): LogOptions[keyof LogOptions];
|
|
82
|
+
chalk(): ReturnType<Color['getChalkInstance']>;
|
|
83
|
+
/**
|
|
84
|
+
* Logs to console, and conditionally to file.
|
|
85
|
+
*
|
|
86
|
+
* When `logToFile` is true this awaits `writeToFile` and propagates its rejection unchanged, so
|
|
87
|
+
* the caller receives the underlying `NodeJS.ErrnoException`. Callers must handle it: see the
|
|
88
|
+
* `writeToFile` contract below.
|
|
89
|
+
*/
|
|
90
|
+
log(content: string, type: string, logToFile: boolean, overwrite: boolean): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Direct hardcoded debug method (limited file logging). Unlike the `defineType` convenience
|
|
93
|
+
* methods this does not route through `logAction`, so it never consults `logToFileByDefault` and
|
|
94
|
+
* only writes to file when `logToFile` is passed explicitly.
|
|
95
|
+
*
|
|
96
|
+
* When `logToFile` is true this awaits `writeToFile` and propagates its rejection unchanged, on
|
|
97
|
+
* exactly the same contract as `log` - see `writeToFile` below.
|
|
98
|
+
*/
|
|
99
|
+
debug(content: unknown, logToFile?: boolean, overwrite?: boolean): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Writes `content` to the resolved target for `type`, creating the target's directory on first
|
|
102
|
+
* use for that directory.
|
|
103
|
+
*
|
|
104
|
+
* The returned promise rejecting with the underlying `NodeJS.ErrnoException` (with `err.code`
|
|
105
|
+
* preserved) is the sole failure signal - callers that care must handle it. The structured
|
|
106
|
+
* stderr notice emitted alongside a failure is informational only and is deduped per episode.
|
|
107
|
+
*
|
|
108
|
+
* On `ENOENT`, `EACCES`, `EPERM` or `EROFS` the cached directory entry is dropped and the write
|
|
109
|
+
* is retried exactly once. Other codes do not themselves trigger a retry; the decision is keyed
|
|
110
|
+
* on the first attempt's code.
|
|
111
|
+
*
|
|
112
|
+
* A rejection is terminal either way. For those four codes the self-heal has already run and
|
|
113
|
+
* failed by the time one escapes; the rest were never retryable, so repeating the call cannot
|
|
114
|
+
* change the outcome. Consumers must not layer their own retry on top - doing so either races the
|
|
115
|
+
* internal one or repeats a call that already cannot succeed, and delays the disable/back-off the
|
|
116
|
+
* rejection exists to trigger. The correct response to the first rejection is to stop writing.
|
|
117
|
+
* See README "Handling write failures".
|
|
118
|
+
*/
|
|
119
|
+
writeToFile(type: string, content: string, overwrite: boolean): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* One structured stderr notice per failure episode: an emit on the first failure, silence while
|
|
122
|
+
* it keeps failing, one recovery emit on the next success. A null payload records a success.
|
|
123
|
+
*
|
|
124
|
+
* Keyed on the directory rather than the resolved target, matching `directoryCache`: every code
|
|
125
|
+
* in the retry allowlist is a directory-level condition, so keying on the target would strand an
|
|
126
|
+
* un-reaped entry - and silently drop its suppressed count - whenever a `{date}` template rotates
|
|
127
|
+
* away from a still-failing filename.
|
|
128
|
+
*/
|
|
129
|
+
noticeWriteResult(path: string, targetLog: string, payload: Record<string, unknown> | null): void;
|
|
130
|
+
/**
|
|
131
|
+
* The write-failure path must never re-enter this logger, so notices go straight out through
|
|
132
|
+
* `console.error` as a single JSONL record, bypassing all of this package's formatting. Field
|
|
133
|
+
* order is the canonical one mandated by the framework logging schema, with the optional
|
|
134
|
+
* `schemaVersion` last.
|
|
135
|
+
*/
|
|
136
|
+
emitNotice(severity: Severity, event: string, payload: Record<string, unknown>): void;
|
|
137
|
+
resolveFilename(template: string, type: string): string;
|
|
138
|
+
/**
|
|
139
|
+
* Ensures the target's directory exists. Both write paths auto-create the file itself, so no
|
|
140
|
+
* bootstrap write is needed. `targetLog` is unused but retained for signature compatibility:
|
|
141
|
+
* resolveFilename strips separators, so the only cached invariant is the directory.
|
|
142
|
+
*/
|
|
143
|
+
validateFileAndDirectory(path: string, targetLog: string): Promise<void>;
|
|
144
|
+
sanitizePath(path: string): string;
|
|
145
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { promises as fsp } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { hostname } from 'os';
|
|
4
|
+
import projectPath from 'path';
|
|
5
|
+
import Color from './color.js';
|
|
6
|
+
const defaultOptions = {
|
|
7
|
+
logToFileByDefault: false,
|
|
8
|
+
logTimestamp: false,
|
|
9
|
+
path: 'logs/',
|
|
10
|
+
prefix: '',
|
|
11
|
+
suffix: '',
|
|
12
|
+
filename: '',
|
|
13
|
+
additionalLogs: {},
|
|
14
|
+
systemLogs: {
|
|
15
|
+
info: 'cyan',
|
|
16
|
+
warn: 'yellow',
|
|
17
|
+
error: 'red',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
// used to sanitize defineType() options input
|
|
21
|
+
const optionKeys = Object.keys(defaultOptions);
|
|
22
|
+
export default class Log {
|
|
23
|
+
options;
|
|
24
|
+
color;
|
|
25
|
+
typeOptions = {};
|
|
26
|
+
// resolved directory -> in-flight or settled mkdir; instance-scoped so it cannot outlive its directory
|
|
27
|
+
directoryCache = new Map();
|
|
28
|
+
// resolved directory -> consecutive write failures, used to dedupe the operator notice
|
|
29
|
+
writeFailures = new Map();
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
const merged = {
|
|
32
|
+
...defaultOptions,
|
|
33
|
+
...options,
|
|
34
|
+
};
|
|
35
|
+
this.options = merged;
|
|
36
|
+
this.options.path = this.sanitizePath(this.options.path);
|
|
37
|
+
const { additionalLogs, systemLogs } = merged;
|
|
38
|
+
const logs = {
|
|
39
|
+
...systemLogs,
|
|
40
|
+
...additionalLogs,
|
|
41
|
+
};
|
|
42
|
+
this.color = new Color();
|
|
43
|
+
this.typeOptions = {};
|
|
44
|
+
// create direct convenience methods for logging
|
|
45
|
+
for (const type of Object.keys(logs)) {
|
|
46
|
+
this.defineType(type, logs[type]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// records setting and options for log type, and creates convenience method ie: log.info()
|
|
50
|
+
defineType(type, setting, options = null) {
|
|
51
|
+
this.color.setLogColor(type, setting);
|
|
52
|
+
// create convenience method if it doesn't exist
|
|
53
|
+
if (!this[type])
|
|
54
|
+
this.createConvenienceMethod(type);
|
|
55
|
+
if (!options)
|
|
56
|
+
return;
|
|
57
|
+
if (typeof options !== 'object')
|
|
58
|
+
throw new Error('The options param must be an object.');
|
|
59
|
+
for (const option of Object.keys(options)) {
|
|
60
|
+
if (!optionKeys.includes(option)) {
|
|
61
|
+
throw new Error(`${option} is not a valid configuration object.`
|
|
62
|
+
+ '\n For a list of available options, see https://github.com/abofs/stonyx-logs#configuration');
|
|
63
|
+
}
|
|
64
|
+
// sanitize path input
|
|
65
|
+
if (option === 'path') {
|
|
66
|
+
options[option] = this.sanitizePath(options[option]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.typeOptions[type] = options;
|
|
70
|
+
}
|
|
71
|
+
// proxy through `logAction` method in order to set defaults based on argument presence
|
|
72
|
+
createConvenienceMethod(type) {
|
|
73
|
+
this[type] = (content, logToFile, overwrite = false) => this.logAction(type, content, logToFile, overwrite);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validates params and applies configuration-based defaults for logging. Every convenience method
|
|
77
|
+
* created by `defineType` - including `info`, `warn` and `error` - routes through here. Note that
|
|
78
|
+
* `debug` does not: it is a direct method that never consults `logToFileByDefault`.
|
|
79
|
+
*
|
|
80
|
+
* **A failed file write rejects the returned promise, and that rejection is terminal - do not retry
|
|
81
|
+
* it.** `ENOENT`, `EACCES`, `EPERM` and `EROFS` have already been retried once internally by the
|
|
82
|
+
* time one reaches you; every other code (`EISDIR`, `ENOTDIR`, `ENOSPC`, `EMFILE`, ...) is never
|
|
83
|
+
* retried, because a retry cannot help it. Handle the first rejection by disabling file logging or
|
|
84
|
+
* backing off - never by writing again. The rejection value is the underlying
|
|
85
|
+
* `NodeJS.ErrnoException` with `err.code` preserved, and leaving it unhandled terminates the
|
|
86
|
+
* process.
|
|
87
|
+
*
|
|
88
|
+
* @see https://github.com/abofs/stonyx-logs#handling-write-failures
|
|
89
|
+
*/
|
|
90
|
+
logAction(type, content, logToFile, overwrite) {
|
|
91
|
+
// set logToFile default based on class options when not set
|
|
92
|
+
if (logToFile === undefined)
|
|
93
|
+
logToFile = this.getOptionForType(type, 'logToFileByDefault');
|
|
94
|
+
// treat overwrite default as true for log type "debug"
|
|
95
|
+
if (type === 'debug' && overwrite === undefined)
|
|
96
|
+
overwrite = true;
|
|
97
|
+
return this.log(content, type, logToFile, overwrite ?? false);
|
|
98
|
+
}
|
|
99
|
+
// retrieves option setting for given type, default to global
|
|
100
|
+
getOptionForType(type, option) {
|
|
101
|
+
const options = this.typeOptions[type];
|
|
102
|
+
if (!options || !options[option])
|
|
103
|
+
return this.options[option];
|
|
104
|
+
return options[option];
|
|
105
|
+
}
|
|
106
|
+
// exposes chalk for custom color options via defineType
|
|
107
|
+
chalk() {
|
|
108
|
+
return this.color.getChalkInstance();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Logs to console, and conditionally to file.
|
|
112
|
+
*
|
|
113
|
+
* When `logToFile` is true this awaits `writeToFile` and propagates its rejection unchanged, so
|
|
114
|
+
* the caller receives the underlying `NodeJS.ErrnoException`. Callers must handle it: see the
|
|
115
|
+
* `writeToFile` contract below.
|
|
116
|
+
*/
|
|
117
|
+
async log(content, type, logToFile, overwrite) {
|
|
118
|
+
const logTimestamp = this.getOptionForType(type, 'logTimestamp');
|
|
119
|
+
const timestamp = `[${new Date().toLocaleString('en-US')}]`;
|
|
120
|
+
const chalkColorFunction = this.color.getLogColor(type);
|
|
121
|
+
let prefix = this.getOptionForType(type, 'prefix');
|
|
122
|
+
let suffix = this.getOptionForType(type, 'suffix');
|
|
123
|
+
if (logTimestamp)
|
|
124
|
+
prefix += `${timestamp} `;
|
|
125
|
+
if (prefix)
|
|
126
|
+
prefix = chalkColorFunction(prefix);
|
|
127
|
+
if (suffix)
|
|
128
|
+
suffix = chalkColorFunction(suffix);
|
|
129
|
+
const coloredLog = chalkColorFunction(content);
|
|
130
|
+
console.log(`${prefix}${coloredLog}${suffix}`); // eslint-disable-line no-console
|
|
131
|
+
if (!logToFile)
|
|
132
|
+
return;
|
|
133
|
+
await this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Direct hardcoded debug method (limited file logging). Unlike the `defineType` convenience
|
|
137
|
+
* methods this does not route through `logAction`, so it never consults `logToFileByDefault` and
|
|
138
|
+
* only writes to file when `logToFile` is passed explicitly.
|
|
139
|
+
*
|
|
140
|
+
* When `logToFile` is true this awaits `writeToFile` and propagates its rejection unchanged, on
|
|
141
|
+
* exactly the same contract as `log` - see `writeToFile` below.
|
|
142
|
+
*/
|
|
143
|
+
async debug(content, logToFile = false, overwrite = true) {
|
|
144
|
+
console.dir(content, { depth: 6 }); // eslint-disable-line no-console
|
|
145
|
+
if (!logToFile)
|
|
146
|
+
return;
|
|
147
|
+
await this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Writes `content` to the resolved target for `type`, creating the target's directory on first
|
|
151
|
+
* use for that directory.
|
|
152
|
+
*
|
|
153
|
+
* The returned promise rejecting with the underlying `NodeJS.ErrnoException` (with `err.code`
|
|
154
|
+
* preserved) is the sole failure signal - callers that care must handle it. The structured
|
|
155
|
+
* stderr notice emitted alongside a failure is informational only and is deduped per episode.
|
|
156
|
+
*
|
|
157
|
+
* On `ENOENT`, `EACCES`, `EPERM` or `EROFS` the cached directory entry is dropped and the write
|
|
158
|
+
* is retried exactly once. Other codes do not themselves trigger a retry; the decision is keyed
|
|
159
|
+
* on the first attempt's code.
|
|
160
|
+
*
|
|
161
|
+
* A rejection is terminal either way. For those four codes the self-heal has already run and
|
|
162
|
+
* failed by the time one escapes; the rest were never retryable, so repeating the call cannot
|
|
163
|
+
* change the outcome. Consumers must not layer their own retry on top - doing so either races the
|
|
164
|
+
* internal one or repeats a call that already cannot succeed, and delays the disable/back-off the
|
|
165
|
+
* rejection exists to trigger. The correct response to the first rejection is to stop writing.
|
|
166
|
+
* See README "Handling write failures".
|
|
167
|
+
*/
|
|
168
|
+
async writeToFile(type, content, overwrite) {
|
|
169
|
+
const path = this.getOptionForType(type, 'path');
|
|
170
|
+
const filenameTemplate = this.getOptionForType(type, 'filename');
|
|
171
|
+
const targetLog = `${path}${this.resolveFilename(filenameTemplate, type)}`;
|
|
172
|
+
const cached = this.directoryCache.has(path);
|
|
173
|
+
const attempt = async () => {
|
|
174
|
+
await this.validateFileAndDirectory(path, targetLog);
|
|
175
|
+
await (overwrite ? fsp.writeFile : fsp.appendFile)(targetLog, content);
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
await attempt().catch(async (error) => {
|
|
179
|
+
// a warm cache can outlive its directory, so drop the entry and retry exactly once
|
|
180
|
+
if (!['ENOENT', 'EACCES', 'EPERM', 'EROFS'].includes(error?.code))
|
|
181
|
+
throw error;
|
|
182
|
+
this.directoryCache.delete(path);
|
|
183
|
+
return attempt();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const { code, syscall } = error;
|
|
188
|
+
this.noticeWriteResult(path, targetLog, {
|
|
189
|
+
targetLog,
|
|
190
|
+
path,
|
|
191
|
+
syscall,
|
|
192
|
+
code,
|
|
193
|
+
cached,
|
|
194
|
+
});
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
this.noticeWriteResult(path, targetLog, null);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* One structured stderr notice per failure episode: an emit on the first failure, silence while
|
|
201
|
+
* it keeps failing, one recovery emit on the next success. A null payload records a success.
|
|
202
|
+
*
|
|
203
|
+
* Keyed on the directory rather than the resolved target, matching `directoryCache`: every code
|
|
204
|
+
* in the retry allowlist is a directory-level condition, so keying on the target would strand an
|
|
205
|
+
* un-reaped entry - and silently drop its suppressed count - whenever a `{date}` template rotates
|
|
206
|
+
* away from a still-failing filename.
|
|
207
|
+
*/
|
|
208
|
+
noticeWriteResult(path, targetLog, payload) {
|
|
209
|
+
const failures = this.writeFailures.get(path) ?? 0;
|
|
210
|
+
if (payload) {
|
|
211
|
+
this.writeFailures.set(path, failures + 1);
|
|
212
|
+
if (!failures) {
|
|
213
|
+
this.emitNotice('error', 'log-write-failed', {
|
|
214
|
+
...payload,
|
|
215
|
+
suppressedCount: 0,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (failures) {
|
|
220
|
+
this.writeFailures.delete(path);
|
|
221
|
+
this.emitNotice('warn', 'log-write-recovered', {
|
|
222
|
+
targetLog,
|
|
223
|
+
path,
|
|
224
|
+
suppressedCount: failures - 1,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The write-failure path must never re-enter this logger, so notices go straight out through
|
|
230
|
+
* `console.error` as a single JSONL record, bypassing all of this package's formatting. Field
|
|
231
|
+
* order is the canonical one mandated by the framework logging schema, with the optional
|
|
232
|
+
* `schemaVersion` last.
|
|
233
|
+
*/
|
|
234
|
+
emitNotice(severity, event, payload) {
|
|
235
|
+
const ts = new Date().toISOString();
|
|
236
|
+
console.error(JSON.stringify({
|
|
237
|
+
ts,
|
|
238
|
+
surface: 'stonyx-logs',
|
|
239
|
+
sessionKey: null,
|
|
240
|
+
project: null,
|
|
241
|
+
severity,
|
|
242
|
+
event,
|
|
243
|
+
payload,
|
|
244
|
+
schemaVersion: 1,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
// resolves template variables in a filename string
|
|
248
|
+
resolveFilename(template, type) {
|
|
249
|
+
// default to '{type}.log' when no template is configured
|
|
250
|
+
if (!template)
|
|
251
|
+
return `${type}.log`;
|
|
252
|
+
const now = new Date();
|
|
253
|
+
const yyyy = now.getFullYear();
|
|
254
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
255
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
256
|
+
const variables = {
|
|
257
|
+
date: `${yyyy}-${mm}-${dd}`,
|
|
258
|
+
type,
|
|
259
|
+
pid: process.pid,
|
|
260
|
+
hostname: hostname(),
|
|
261
|
+
};
|
|
262
|
+
const resolved = template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
263
|
+
return variables[key] !== undefined ? String(variables[key]) : match;
|
|
264
|
+
});
|
|
265
|
+
// sanitize: prevent path traversal and disallow directory separators
|
|
266
|
+
return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Ensures the target's directory exists. Both write paths auto-create the file itself, so no
|
|
270
|
+
* bootstrap write is needed. `targetLog` is unused but retained for signature compatibility:
|
|
271
|
+
* resolveFilename strips separators, so the only cached invariant is the directory.
|
|
272
|
+
*/
|
|
273
|
+
async validateFileAndDirectory(path, targetLog) {
|
|
274
|
+
let pending = this.directoryCache.get(path);
|
|
275
|
+
if (!pending) {
|
|
276
|
+
// cache the promise before awaiting: a flag here would let a concurrent write append first
|
|
277
|
+
pending = fsp.mkdir(path, { recursive: true }).then(() => undefined);
|
|
278
|
+
this.directoryCache.set(path, pending);
|
|
279
|
+
// never keep a poisoned entry - the next write retries from scratch
|
|
280
|
+
pending.catch(() => this.directoryCache.delete(path));
|
|
281
|
+
}
|
|
282
|
+
return pending;
|
|
283
|
+
}
|
|
284
|
+
// method to conditionally sanitize user configuration input
|
|
285
|
+
sanitizePath(path) {
|
|
286
|
+
const moduleDir = projectPath.dirname(fileURLToPath(import.meta.url));
|
|
287
|
+
const delim = moduleDir.includes('node_modules') ? 'node_modules' : 'src';
|
|
288
|
+
const splitDir = moduleDir.split(delim);
|
|
289
|
+
if (splitDir.length < 2)
|
|
290
|
+
throw new Error('Failed to locate your project\'s root directory.');
|
|
291
|
+
// use project root directory behind path
|
|
292
|
+
path = projectPath.resolve(splitDir[0], path);
|
|
293
|
+
// force path property to contain a trailing "/"
|
|
294
|
+
if (path[path.length - 1] !== '/') {
|
|
295
|
+
path += '/';
|
|
296
|
+
}
|
|
297
|
+
return path;
|
|
298
|
+
}
|
|
299
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonyx/logs",
|
|
3
|
-
"version": "1.0.1-alpha.
|
|
3
|
+
"version": "1.0.1-alpha.20",
|
|
4
4
|
"description": "Simplified logging for node applications",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./color": {
|
|
14
|
+
"types": "./dist/color.d.ts",
|
|
15
|
+
"default": "./dist/color.js"
|
|
16
|
+
}
|
|
10
17
|
},
|
|
11
18
|
"files": [
|
|
12
|
-
"
|
|
19
|
+
"dist",
|
|
13
20
|
"README.md"
|
|
14
21
|
],
|
|
15
22
|
"publishConfig": {
|
|
@@ -24,7 +31,7 @@
|
|
|
24
31
|
"log",
|
|
25
32
|
"logging",
|
|
26
33
|
"color-coding",
|
|
27
|
-
"
|
|
34
|
+
"stonyx",
|
|
28
35
|
"history",
|
|
29
36
|
"documentation",
|
|
30
37
|
"document",
|
|
@@ -43,13 +50,20 @@
|
|
|
43
50
|
"chalk": "^5.3.0"
|
|
44
51
|
},
|
|
45
52
|
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.5.2",
|
|
54
|
+
"@types/qunit": "^2.19.13",
|
|
55
|
+
"@types/sinon": "^21.0.1",
|
|
46
56
|
"eslint": "^8.27.0",
|
|
47
57
|
"eslint-plugin-node": "^11.1.0",
|
|
48
58
|
"qunit": "^2.19.3",
|
|
49
|
-
"sinon": "^17.0.0"
|
|
59
|
+
"sinon": "^17.0.0",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
|
+
"typescript": "^5.8.3"
|
|
50
62
|
},
|
|
51
63
|
"scripts": {
|
|
52
|
-
"
|
|
64
|
+
"build": "tsc",
|
|
65
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
66
|
+
"test": "node --import tsx node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'",
|
|
53
67
|
"lint": "eslint . --fix"
|
|
54
68
|
}
|
|
55
69
|
}
|
package/src/color.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
export default class Color {
|
|
4
|
-
constructor() {
|
|
5
|
-
this.types = [];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// retrieves configured color function for log type
|
|
9
|
-
getLogColor(type) {
|
|
10
|
-
return this.types[type];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
getChalkInstance() {
|
|
14
|
-
return chalk;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
setLogColor(type, setting) {
|
|
18
|
-
const chalkColorFunction = this.settingToChalkColorFunction(setting);
|
|
19
|
-
this.types[type] = chalkColorFunction;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// retrieves chalk color function, and fully validates output
|
|
23
|
-
settingToChalkColorFunction(setting) {
|
|
24
|
-
const errorMessage = 'Invalid chalk color function.'
|
|
25
|
-
+ 'For help with color settings, see https://github.com/abofs/chronicle#defining-logs--colors';
|
|
26
|
-
|
|
27
|
-
switch (typeof setting) {
|
|
28
|
-
case 'string':
|
|
29
|
-
const chalkColorFunction = (setting[0] === '#') ? chalk.hex(setting) : chalk[setting];
|
|
30
|
-
if (!chalkColorFunction
|
|
31
|
-
|| typeof chalkColorFunction !== 'function'
|
|
32
|
-
|| typeof chalkColorFunction('') !== 'string') {
|
|
33
|
-
throw errorMessage;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return chalkColorFunction;
|
|
37
|
-
|
|
38
|
-
case 'function':
|
|
39
|
-
// validate that given function returns a string
|
|
40
|
-
if (typeof setting('') !== 'string') {
|
|
41
|
-
throw errorMessage;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return setting;
|
|
45
|
-
|
|
46
|
-
default:
|
|
47
|
-
throw errorMessage;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, promises as fsp } from 'fs';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
|
-
import projectPath from 'path';
|
|
4
|
-
import Color from './color.js';
|
|
5
|
-
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
logToFileByDefault: false, // default setting (overridable by logToFile param)
|
|
8
|
-
logTimestamp: false, // option to include timestamp in console logs
|
|
9
|
-
path: 'logs/', // default log directory (relative to main project root directory)
|
|
10
|
-
prefix: '',
|
|
11
|
-
suffix: '',
|
|
12
|
-
|
|
13
|
-
// log types with corresponding color settings
|
|
14
|
-
additionalLogs: {},
|
|
15
|
-
systemLogs: {
|
|
16
|
-
info: 'cyan',
|
|
17
|
-
warn: 'yellow',
|
|
18
|
-
error: 'red',
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// used to sanitize defineType() options input
|
|
23
|
-
const optionKeys = Object.keys(defaultOptions);
|
|
24
|
-
|
|
25
|
-
export default class Chronicle {
|
|
26
|
-
constructor(options = defaultOptions) {
|
|
27
|
-
options = {
|
|
28
|
-
...defaultOptions,
|
|
29
|
-
...options,
|
|
30
|
-
};
|
|
31
|
-
this.options = options;
|
|
32
|
-
this.options.path = this.sanitizePath(this.options.path);
|
|
33
|
-
|
|
34
|
-
const { additionalLogs, systemLogs } = options;
|
|
35
|
-
const logs = {
|
|
36
|
-
...systemLogs,
|
|
37
|
-
...additionalLogs,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
this.color = new Color();
|
|
41
|
-
this.typeOptions = [];
|
|
42
|
-
|
|
43
|
-
// create direct convenience methods for logging
|
|
44
|
-
for (const type of Object.keys(logs)) {
|
|
45
|
-
this.defineType(type, logs[type]);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// records setting and options for log type, and crates convenience method ie: chronicle.info()
|
|
50
|
-
defineType(type, setting, options = null) {
|
|
51
|
-
this.color.setLogColor(type, setting);
|
|
52
|
-
|
|
53
|
-
// create convenience method if it doesn't exist
|
|
54
|
-
if (!this[type]) this.createConvenienceMethod(type);
|
|
55
|
-
|
|
56
|
-
if (!options) return;
|
|
57
|
-
if (typeof options !== 'object') throw 'The options param must be an object.';
|
|
58
|
-
|
|
59
|
-
for (let option of Object.keys(options)) {
|
|
60
|
-
if (!optionKeys.includes(option)) {
|
|
61
|
-
throw `${option} is not a valid configuration object.`
|
|
62
|
-
+ '\n For a list of available options, see https://github.com/abofs/chronicle#configuration';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// sanitize path input
|
|
66
|
-
if (option === 'path') options[option] = this.sanitizePath(options[option]);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
this.typeOptions[type] = options;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// proxy through `logAction` method in order to set defaults based on argument presence
|
|
73
|
-
createConvenienceMethod(type) {
|
|
74
|
-
this[type] = (content, logToFile, overwrite = false) =>
|
|
75
|
-
this.logAction(type, content, logToFile, overwrite);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// validates params and sets configuration-based defaults for logging
|
|
79
|
-
logAction(type, content, logToFile, overwrite) {
|
|
80
|
-
// set logToFile default based on class options when not set
|
|
81
|
-
if (arguments[2] === undefined) logToFile = this.getOptionForType(type, 'logToFileByDefault');
|
|
82
|
-
|
|
83
|
-
// treat overwrite default as true for log type "debug"
|
|
84
|
-
if (type === 'debug' && arguments[3] === undefined) overwrite = true;
|
|
85
|
-
|
|
86
|
-
return this.log(content, type, logToFile, overwrite);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// retrieves option setting for given type, default to global
|
|
90
|
-
getOptionForType(type, option) {
|
|
91
|
-
const options = this.typeOptions[type];
|
|
92
|
-
if (!options || !options[option]) return this.options[option];
|
|
93
|
-
|
|
94
|
-
return options[option];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// exposes chalk for custom color options via defineType
|
|
98
|
-
chalk() {
|
|
99
|
-
return this.color.getChalkInstance();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// logs to console, and conditionally to file
|
|
103
|
-
async log(content, type, logToFile, overwrite) {
|
|
104
|
-
const logTimestamp = this.getOptionForType(type, 'logTimestamp');
|
|
105
|
-
const timestamp = `[${new Date().toLocaleString('en-US')}]`;
|
|
106
|
-
const chalkColorFunction = this.color.getLogColor(type);
|
|
107
|
-
let prefix = this.getOptionForType(type, 'prefix');
|
|
108
|
-
let suffix = this.getOptionForType(type, 'suffix');
|
|
109
|
-
if (logTimestamp) prefix += `${timestamp} `;
|
|
110
|
-
if (prefix) prefix = chalkColorFunction(prefix);
|
|
111
|
-
if (suffix) suffix = chalkColorFunction(suffix);
|
|
112
|
-
const coloredLog = chalkColorFunction(content);
|
|
113
|
-
|
|
114
|
-
console.log(`${prefix}${coloredLog}${suffix}`); // eslint-disable-line no-console
|
|
115
|
-
|
|
116
|
-
if (!logToFile) return;
|
|
117
|
-
|
|
118
|
-
return this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// direct hardcoded debug method (log to file functionality is limited)
|
|
122
|
-
async debug(content, logToFile = false, overwrite = true) {
|
|
123
|
-
console.dir(content, { depth: 6 }); // eslint-disable-line no-console
|
|
124
|
-
|
|
125
|
-
if (!logToFile) return;
|
|
126
|
-
|
|
127
|
-
return this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async writeToFile(type, content, overwrite) {
|
|
131
|
-
const path = this.getOptionForType(type, 'path');
|
|
132
|
-
const targetLog = `${path}${type}.log`;
|
|
133
|
-
await this.validateFileAndDirectory(path, targetLog);
|
|
134
|
-
|
|
135
|
-
const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
|
|
136
|
-
|
|
137
|
-
return fileAction(targetLog, content);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// attempts to create file and/or directory if they don't already exist
|
|
141
|
-
async validateFileAndDirectory(path, targetLog) {
|
|
142
|
-
const errorMethod = this.error || console.error; // prefer native method unless removed by user
|
|
143
|
-
|
|
144
|
-
mkdirSync(path, { recursive: true });
|
|
145
|
-
|
|
146
|
-
await fsp.access(targetLog).catch(() => {
|
|
147
|
-
fsp.writeFile(targetLog, '').catch(() => {
|
|
148
|
-
errorMethod(`Failed to create log file: ${targetLog}.`
|
|
149
|
-
+ '\n Verify that the application runner has write permissions');
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// method to conditionally sanitize user configuration input
|
|
155
|
-
sanitizePath(path) {
|
|
156
|
-
const moduleDir = projectPath.dirname(fileURLToPath(import.meta.url));
|
|
157
|
-
const delim = moduleDir.includes('node_modules') ? 'node_modules' : 'src';
|
|
158
|
-
const splitDir = moduleDir.split(delim);
|
|
159
|
-
|
|
160
|
-
if (splitDir.length < 2) throw ('Failed to locate your project\'s root directory.');
|
|
161
|
-
|
|
162
|
-
// use project root directory behind path
|
|
163
|
-
path = projectPath.resolve(splitDir[0], path);
|
|
164
|
-
|
|
165
|
-
// force path property to contain a trailing "/"
|
|
166
|
-
if (path[path.length - 1] !== '/') {
|
|
167
|
-
path += '/';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return path;
|
|
171
|
-
}
|
|
172
|
-
}
|