@stonyx/logs 1.0.1-beta.19 → 1.0.1-beta.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 CHANGED
@@ -57,11 +57,7 @@ const log = new Log();
57
57
 
58
58
  log.info('Info: sample application has started');
59
59
  log.warn('Warning: this is just a sample');
60
-
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' */ });
60
+ log.error('Error: no application logic detected', true); // logs to logs/error.log file
65
61
  ```
66
62
 
67
63
  Easily define your own logging mechanism and color-coding preference:
@@ -95,15 +91,8 @@ const log = new Log({
95
91
  suffix: '\n=============================================================== \n',
96
92
  });
97
93
 
98
- log.info('Info: sample application has started')
99
- .catch(err => { /* see "Handling write failures" */ });
94
+ log.info('Info: sample application has started');
100
95
  ```
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
-
107
96
  ![](https://github.com/abofs/stonyx-logs/raw/main/media/examples/custom-options.jpg)
108
97
 
109
98
 
@@ -118,7 +107,6 @@ const log = new Log({ additionalLogs: { question: 'green' } });
118
107
  log.defineType('query', log.chalk().black.bgGreen);
119
108
 
120
109
  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
110
  await log.query('This is what a custom chalk color setting looks like', true);
123
111
  ```
124
112
  ![](https://github.com/abofs/stonyx-logs/raw/main/media/examples/additional-logs.jpg)
@@ -165,20 +153,13 @@ These methods can then be called in your application with [logging parameters](#
165
153
 
166
154
  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.
167
155
 
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.
156
+ Additionally, these methods return a promise when `logToFile` is true, allowing you use them with `await` in an async method, or append `then(), catch(), or finally()` for more advanced callback usage.
172
157
 
173
158
  ```js
174
159
  async method() {
175
- try {
176
- await log.error('error message', true);
160
+ await log.error('error message', true);
177
161
 
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
- }
162
+ // do something after logs/error.log (default) is created
182
163
  }
183
164
  ```
184
165
 
@@ -196,11 +177,6 @@ JSON.stringify(content, null, 2);
196
177
 
197
178
  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
179
 
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).
203
-
204
180
  ### Logging Parameters
205
181
 
206
182
  ```js
@@ -210,161 +186,10 @@ log.error('error message', true, false); // content, logToFile, overwrite
210
186
  | Parameter | Type | Default | Description |
211
187
  | :---: | :---: | :---: | :--- |
212
188
  | `content` | **String** | | Content of log that will output on your console. |
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). |
189
+ | `logToFile` | **Boolean** | *false* | Option to log content to file. |
214
190
  | `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. |
215
191
 
216
192
  **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
-
368
193
  ### Configuration
369
194
 
370
195
  When instantiating **Log**, you can pass an object to customize your settings. Below is the default configuration:
@@ -388,7 +213,7 @@ const log = new Log({
388
213
 
389
214
  | Option | Type | Default | Description |
390
215
  | :---: | :---: | :---: | :--- |
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). |
216
+ | `logToFileByDefault` | **Boolean** | *false* | Option to change default setting for `logToFile` parameter of logging functions. |
392
217
  | `logTimestamp` | **Boolean** | *false* | Option to include timestamp in console logging. Timestamps are automatically included in file logs. |
393
218
  | `path` | **String** | *'logs/'* | Path in which to store log files. This setting is relative to your project's root directory. |
394
219
  | `prefix` | **String** | *''* | Prefix string to prepend all log messages for all log types with the exception of *debug*. |
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import Color, { type ColorSetting } from './color.js';
2
- export type Severity = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
3
2
  export interface LogOptions {
4
3
  logToFileByDefault: boolean;
5
4
  logTimestamp: boolean;
@@ -14,132 +13,20 @@ export default class Log {
14
13
  options: LogOptions;
15
14
  color: Color;
16
15
  typeOptions: Record<string, Partial<LogOptions>>;
17
- directoryCache: Map<string, Promise<void>>;
18
- writeFailures: Map<string, number>;
19
16
  [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
17
  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
18
  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
19
  error: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
62
20
  constructor(options?: Partial<LogOptions>);
63
21
  defineType(type: string, setting: ColorSetting, options?: Partial<LogOptions> | null): void;
64
22
  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
23
  logAction(type: string, content: string, logToFile?: boolean, overwrite?: boolean): Promise<void>;
81
24
  getOptionForType(type: string, option: keyof LogOptions): LogOptions[keyof LogOptions];
82
25
  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
26
  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
27
  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
28
  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
29
  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
30
  validateFileAndDirectory(path: string, targetLog: string): Promise<void>;
144
31
  sanitizePath(path: string): string;
145
32
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { promises as fsp } from 'fs';
1
+ import { mkdirSync, promises as fsp } from 'fs';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { hostname } from 'os';
4
4
  import projectPath from 'path';
@@ -23,10 +23,6 @@ export default class Log {
23
23
  options;
24
24
  color;
25
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
26
  constructor(options = {}) {
31
27
  const merged = {
32
28
  ...defaultOptions,
@@ -72,21 +68,7 @@ export default class Log {
72
68
  createConvenienceMethod(type) {
73
69
  this[type] = (content, logToFile, overwrite = false) => this.logAction(type, content, logToFile, overwrite);
74
70
  }
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
- */
71
+ // validates params and sets configuration-based defaults for logging
90
72
  logAction(type, content, logToFile, overwrite) {
91
73
  // set logToFile default based on class options when not set
92
74
  if (logToFile === undefined)
@@ -107,13 +89,7 @@ export default class Log {
107
89
  chalk() {
108
90
  return this.color.getChalkInstance();
109
91
  }
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
- */
92
+ // logs to console, and conditionally to file
117
93
  async log(content, type, logToFile, overwrite) {
118
94
  const logTimestamp = this.getOptionForType(type, 'logTimestamp');
119
95
  const timestamp = `[${new Date().toLocaleString('en-US')}]`;
@@ -132,117 +108,21 @@ export default class Log {
132
108
  return;
133
109
  await this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
134
110
  }
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
- */
111
+ // direct hardcoded debug method (log to file functionality is limited)
143
112
  async debug(content, logToFile = false, overwrite = true) {
144
113
  console.dir(content, { depth: 6 }); // eslint-disable-line no-console
145
114
  if (!logToFile)
146
115
  return;
147
116
  await this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
148
117
  }
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
118
  async writeToFile(type, content, overwrite) {
169
119
  const path = this.getOptionForType(type, 'path');
170
120
  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
- }));
121
+ const resolvedName = this.resolveFilename(filenameTemplate, type);
122
+ const targetLog = `${path}${resolvedName}`;
123
+ await this.validateFileAndDirectory(path, targetLog);
124
+ const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
125
+ await fileAction(targetLog, content);
246
126
  }
247
127
  // resolves template variables in a filename string
248
128
  resolveFilename(template, type) {
@@ -265,21 +145,16 @@ export default class Log {
265
145
  // sanitize: prevent path traversal and disallow directory separators
266
146
  return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
267
147
  }
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
- */
148
+ // attempts to create file and/or directory if they don't already exist
273
149
  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;
150
+ const errorMethod = this.error;
151
+ mkdirSync(path, { recursive: true });
152
+ await fsp.access(targetLog).catch(() => {
153
+ fsp.writeFile(targetLog, '').catch(() => {
154
+ errorMethod(`Failed to create log file: ${targetLog}.`
155
+ + '\n Verify that the application runner has write permissions');
156
+ });
157
+ });
283
158
  }
284
159
  // method to conditionally sanitize user configuration input
285
160
  sanitizePath(path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonyx/logs",
3
- "version": "1.0.1-beta.19",
3
+ "version": "1.0.1-beta.20",
4
4
  "description": "Simplified logging for node applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",