@stonyx/logs 1.0.1-alpha.16 → 1.0.1-alpha.17
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/dist/index.d.ts +3 -2
- package/dist/index.js +66 -19
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Color, { type ColorSetting } from './color.js';
|
|
2
|
+
export type Severity = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
2
3
|
export interface LogOptions {
|
|
3
4
|
logToFileByDefault: boolean;
|
|
4
5
|
logTimestamp: boolean;
|
|
@@ -28,8 +29,8 @@ export default class Log {
|
|
|
28
29
|
log(content: string, type: string, logToFile: boolean, overwrite: boolean): Promise<void>;
|
|
29
30
|
debug(content: unknown, logToFile?: boolean, overwrite?: boolean): Promise<void>;
|
|
30
31
|
writeToFile(type: string, content: string, overwrite: boolean): Promise<void>;
|
|
31
|
-
noticeWriteResult(targetLog: string, payload: Record<string, unknown> | null): void;
|
|
32
|
-
emitNotice(severity:
|
|
32
|
+
noticeWriteResult(path: string, targetLog: string, payload: Record<string, unknown> | null): void;
|
|
33
|
+
emitNotice(severity: Severity, event: string, payload: Record<string, unknown>): void;
|
|
33
34
|
resolveFilename(template: string, type: string): string;
|
|
34
35
|
validateFileAndDirectory(path: string, targetLog: string): Promise<void>;
|
|
35
36
|
sanitizePath(path: string): string;
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export default class Log {
|
|
|
25
25
|
typeOptions = {};
|
|
26
26
|
// resolved directory -> in-flight or settled mkdir; instance-scoped so it cannot outlive its directory
|
|
27
27
|
directoryCache = new Map();
|
|
28
|
-
// resolved
|
|
28
|
+
// resolved directory -> consecutive write failures, used to dedupe the operator notice
|
|
29
29
|
writeFailures = new Map();
|
|
30
30
|
constructor(options = {}) {
|
|
31
31
|
const merged = {
|
|
@@ -93,7 +93,7 @@ export default class Log {
|
|
|
93
93
|
chalk() {
|
|
94
94
|
return this.color.getChalkInstance();
|
|
95
95
|
}
|
|
96
|
-
// logs to console, and conditionally to file
|
|
96
|
+
// logs to console, and conditionally to file; propagates any writeToFile rejection to the caller
|
|
97
97
|
async log(content, type, logToFile, overwrite) {
|
|
98
98
|
const logTimestamp = this.getOptionForType(type, 'logTimestamp');
|
|
99
99
|
const timestamp = `[${new Date().toLocaleString('en-US')}]`;
|
|
@@ -112,13 +112,24 @@ export default class Log {
|
|
|
112
112
|
return;
|
|
113
113
|
await this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
|
|
114
114
|
}
|
|
115
|
-
// direct hardcoded debug method (
|
|
115
|
+
// direct hardcoded debug method (limited file logging); propagates any writeToFile rejection
|
|
116
116
|
async debug(content, logToFile = false, overwrite = true) {
|
|
117
117
|
console.dir(content, { depth: 6 }); // eslint-disable-line no-console
|
|
118
118
|
if (!logToFile)
|
|
119
119
|
return;
|
|
120
120
|
await this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
|
|
121
121
|
}
|
|
122
|
+
/*
|
|
123
|
+
* Writes `content` to the resolved target for `type`, creating the target's directory on first
|
|
124
|
+
* use for that directory.
|
|
125
|
+
*
|
|
126
|
+
* The returned promise rejecting with the underlying `NodeJS.ErrnoException` (with `err.code`
|
|
127
|
+
* preserved) is the sole failure signal - callers that care must handle it. The structured
|
|
128
|
+
* stderr notice emitted alongside a failure is informational only and is deduped per episode.
|
|
129
|
+
*
|
|
130
|
+
* On `ENOENT`, `EACCES`, `EPERM` or `EROFS` the cached directory entry is dropped and the write
|
|
131
|
+
* is retried exactly once; every other code rejects on the first attempt without a retry.
|
|
132
|
+
*/
|
|
122
133
|
async writeToFile(type, content, overwrite) {
|
|
123
134
|
const path = this.getOptionForType(type, 'path');
|
|
124
135
|
const filenameTemplate = this.getOptionForType(type, 'filename');
|
|
@@ -139,29 +150,63 @@ export default class Log {
|
|
|
139
150
|
}
|
|
140
151
|
catch (error) {
|
|
141
152
|
const { code, syscall } = error;
|
|
142
|
-
this.noticeWriteResult(
|
|
153
|
+
this.noticeWriteResult(path, targetLog, {
|
|
154
|
+
targetLog,
|
|
155
|
+
path,
|
|
156
|
+
syscall,
|
|
157
|
+
code,
|
|
158
|
+
cached,
|
|
159
|
+
});
|
|
143
160
|
throw error;
|
|
144
161
|
}
|
|
145
|
-
this.noticeWriteResult(targetLog, null);
|
|
162
|
+
this.noticeWriteResult(path, targetLog, null);
|
|
146
163
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
/*
|
|
165
|
+
* One structured stderr notice per failure episode: an emit on the first failure, silence while
|
|
166
|
+
* it keeps failing, one recovery emit on the next success. A null payload records a success.
|
|
167
|
+
*
|
|
168
|
+
* Keyed on the directory rather than the resolved target, matching `directoryCache`: every code
|
|
169
|
+
* in the retry allowlist is a directory-level condition, so keying on the target would strand an
|
|
170
|
+
* un-reaped entry - and silently drop its suppressed count - whenever a `{date}` template rotates
|
|
171
|
+
* away from a still-failing filename.
|
|
172
|
+
*/
|
|
173
|
+
noticeWriteResult(path, targetLog, payload) {
|
|
174
|
+
const failures = this.writeFailures.get(path) ?? 0;
|
|
151
175
|
if (payload) {
|
|
152
|
-
this.writeFailures.set(
|
|
153
|
-
if (!failures)
|
|
154
|
-
this.emitNotice('error', 'log-write-failed', {
|
|
176
|
+
this.writeFailures.set(path, failures + 1);
|
|
177
|
+
if (!failures) {
|
|
178
|
+
this.emitNotice('error', 'log-write-failed', {
|
|
179
|
+
...payload,
|
|
180
|
+
suppressedCount: 0,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
155
183
|
}
|
|
156
184
|
else if (failures) {
|
|
157
|
-
this.writeFailures.delete(
|
|
158
|
-
this.emitNotice('warn', 'log-write-recovered', {
|
|
185
|
+
this.writeFailures.delete(path);
|
|
186
|
+
this.emitNotice('warn', 'log-write-recovered', {
|
|
187
|
+
targetLog,
|
|
188
|
+
path,
|
|
189
|
+
suppressedCount: failures - 1,
|
|
190
|
+
});
|
|
159
191
|
}
|
|
160
192
|
}
|
|
161
|
-
|
|
193
|
+
/*
|
|
194
|
+
* The write-failure path must never re-enter this logger, so notices go straight to stderr as a
|
|
195
|
+
* single JSONL record. Field order is the canonical one mandated by the framework logging
|
|
196
|
+
* schema, with the optional `schemaVersion` last.
|
|
197
|
+
*/
|
|
162
198
|
emitNotice(severity, event, payload) {
|
|
163
199
|
const ts = new Date().toISOString();
|
|
164
|
-
console.error(JSON.stringify({
|
|
200
|
+
console.error(JSON.stringify({
|
|
201
|
+
ts,
|
|
202
|
+
surface: 'stonyx-logs',
|
|
203
|
+
sessionKey: null,
|
|
204
|
+
project: null,
|
|
205
|
+
severity,
|
|
206
|
+
event,
|
|
207
|
+
payload,
|
|
208
|
+
schemaVersion: 1,
|
|
209
|
+
}));
|
|
165
210
|
}
|
|
166
211
|
// resolves template variables in a filename string
|
|
167
212
|
resolveFilename(template, type) {
|
|
@@ -184,9 +229,11 @@ export default class Log {
|
|
|
184
229
|
// sanitize: prevent path traversal and disallow directory separators
|
|
185
230
|
return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
|
|
186
231
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
232
|
+
/*
|
|
233
|
+
* Ensures the target's directory exists. Both write paths auto-create the file itself, so no
|
|
234
|
+
* bootstrap write is needed. `targetLog` is unused but retained for signature compatibility:
|
|
235
|
+
* resolveFilename strips separators, so the only cached invariant is the directory.
|
|
236
|
+
*/
|
|
190
237
|
async validateFileAndDirectory(path, targetLog) {
|
|
191
238
|
let pending = this.directoryCache.get(path);
|
|
192
239
|
if (!pending) {
|