@diveinto/obs 1.0.4 → 1.0.5
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 +10 -2
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +15 -2
- package/dist/sinks.d.ts +24 -2
- package/dist/sinks.js +27 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,12 @@ of the four, and each of those behaviours is pinned by a test.
|
|
|
35
35
|
writer on the same file, and the two then rename it out from under each
|
|
36
36
|
other at the rotation boundary. Rotation also refuses to overwrite an
|
|
37
37
|
existing rotated file, so a day of records cannot be lost to a rename.
|
|
38
|
+
A single file also has a **ceiling** (256 MB by default,
|
|
39
|
+
`<PREFIX>_LOG_JSON_MAX_MB` / `_TEXT_MAX_MB`, 0 to remove): daily rotation
|
|
40
|
+
bounds how many files there are, not how large one can get, and a service
|
|
41
|
+
that drops to DEBUG or loops on an error can write more in an afternoon
|
|
42
|
+
than the fortnight around it. Worst case per service is now a number an
|
|
43
|
+
operator can reason about rather than the free space on the disk.
|
|
38
44
|
- **Trace context** on `AsyncLocalStorage`, continuing an inbound W3C
|
|
39
45
|
`traceparent` and propagating it outbound, so one `trace_id` spans every
|
|
40
46
|
hop.
|
|
@@ -100,8 +106,10 @@ Generic, never prefixed: `LOG_LEVEL` (`trace`/`debug`/`info`/`warn`/`error`/
|
|
|
100
106
|
`OTEL_*` set.
|
|
101
107
|
|
|
102
108
|
Per service, prefixed: `<PREFIX>_VAR_DIR`, `<PREFIX>_LOG_TEXT_FILE`,
|
|
103
|
-
`_TEXT_ROTATION`, `_TEXT_RETENTION`, and the same
|
|
104
|
-
`*_FILE` value is a path, or `1` for the default path, or
|
|
109
|
+
`_TEXT_ROTATION`, `_TEXT_RETENTION`, `_TEXT_MAX_MB`, and the same four for
|
|
110
|
+
`_JSON_`. A `*_FILE` value is a path, or `1` for the default path, or
|
|
111
|
+
`0`/`off`; `*_MAX_MB` is the per-file ceiling, 256 by default and 0 for
|
|
112
|
+
none.
|
|
105
113
|
|
|
106
114
|
## Cardinality
|
|
107
115
|
|
package/dist/setup.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type FileTarget = {
|
|
|
5
5
|
path: string;
|
|
6
6
|
rotation: string;
|
|
7
7
|
retention: number;
|
|
8
|
+
/** Hard ceiling on one file, bytes; 0 for none. See maxMB below. */
|
|
9
|
+
maxBytes: number;
|
|
8
10
|
};
|
|
9
11
|
/**
|
|
10
12
|
* Resolve where the two files go, as a pure function of the environment so it
|
package/dist/setup.js
CHANGED
|
@@ -42,6 +42,17 @@ function stateDir(env) {
|
|
|
42
42
|
const cfg = obsConfig();
|
|
43
43
|
return env[`${cfg.envPrefix}_VAR_DIR`] || cfg.defaultVarDir;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The default ceiling on a single log file, in megabytes.
|
|
47
|
+
*
|
|
48
|
+
* Daily rotation bounds the NUMBER of files, not the size of one. A service
|
|
49
|
+
* that drops to DEBUG or loops on an error can write more in an afternoon
|
|
50
|
+
* than the fortnight around it, and the first thing to notice would be the
|
|
51
|
+
* disk. 256 MB a file, times fourteen days, times two files is about 7 GB
|
|
52
|
+
* worst case per service, which is a bound an operator can reason about.
|
|
53
|
+
* `<PREFIX>_LOG_JSON_MAX_MB` / `_TEXT_MAX_MB` change it; 0 removes it.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_MAX_MB = 256;
|
|
45
56
|
/**
|
|
46
57
|
* Resolve where the two files go, as a pure function of the environment so it
|
|
47
58
|
* can be tested without touching the filesystem. Defaults land under
|
|
@@ -60,6 +71,7 @@ export function resolveFileTargets(env = process.env) {
|
|
|
60
71
|
path: text === true ? join(dir, `${cfg.fileStem}.log`) : text,
|
|
61
72
|
rotation: (env[`${p}_LOG_TEXT_ROTATION`] || "daily").trim(),
|
|
62
73
|
retention: toInt(env[`${p}_LOG_TEXT_RETENTION`], 14),
|
|
74
|
+
maxBytes: toInt(env[`${p}_LOG_TEXT_MAX_MB`], DEFAULT_MAX_MB) * 1024 * 1024,
|
|
63
75
|
}
|
|
64
76
|
: null,
|
|
65
77
|
json: jsonFile
|
|
@@ -67,6 +79,7 @@ export function resolveFileTargets(env = process.env) {
|
|
|
67
79
|
path: jsonFile === true ? join(dir, `${cfg.fileStem}.jsonl`) : jsonFile,
|
|
68
80
|
rotation: (env[`${p}_LOG_JSON_ROTATION`] || "daily").trim(),
|
|
69
81
|
retention: toInt(env[`${p}_LOG_JSON_RETENTION`], 14),
|
|
82
|
+
maxBytes: toInt(env[`${p}_LOG_JSON_MAX_MB`], DEFAULT_MAX_MB) * 1024 * 1024,
|
|
70
83
|
}
|
|
71
84
|
: null,
|
|
72
85
|
};
|
|
@@ -130,10 +143,10 @@ export function setupLogging() {
|
|
|
130
143
|
if (!serverless && isConfigured()) {
|
|
131
144
|
const targets = resolveFileTargets();
|
|
132
145
|
if (targets.text) {
|
|
133
|
-
sinks.push(makeFileSink(targets.text.path, "pretty", renderPretty, targets.text.rotation, targets.text.retention));
|
|
146
|
+
sinks.push(makeFileSink(targets.text.path, "pretty", renderPretty, targets.text.rotation, targets.text.retention, targets.text.maxBytes));
|
|
134
147
|
}
|
|
135
148
|
if (targets.json) {
|
|
136
|
-
sinks.push(makeFileSink(targets.json.path, "json", renderJson, targets.json.rotation, targets.json.retention));
|
|
149
|
+
sinks.push(makeFileSink(targets.json.path, "json", renderJson, targets.json.rotation, targets.json.retention, targets.json.maxBytes));
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
// When OTLP export is on, also ship logs to the backend's Logs view
|
package/dist/sinks.d.ts
CHANGED
|
@@ -24,11 +24,33 @@ export declare class RotatingFileWriter {
|
|
|
24
24
|
private readonly rotation;
|
|
25
25
|
private readonly retention;
|
|
26
26
|
private readonly render;
|
|
27
|
+
/**
|
|
28
|
+
* A hard ceiling on ONE file, in bytes, on top of time rotation.
|
|
29
|
+
*
|
|
30
|
+
* Daily rotation bounds how many files there are, not how big one can
|
|
31
|
+
* get: a service that starts logging at DEBUG, or loops on an error,
|
|
32
|
+
* can write far more in a day than the fourteen quiet days around it,
|
|
33
|
+
* and nothing stops it before the disk does. With this, the day rolls
|
|
34
|
+
* early instead. Zero disables it, which is what a size-based
|
|
35
|
+
* `rotation` already means.
|
|
36
|
+
*/
|
|
37
|
+
private readonly maxBytes;
|
|
27
38
|
private stream;
|
|
28
39
|
private bytes;
|
|
29
40
|
private periodKey;
|
|
30
41
|
private disabled;
|
|
31
|
-
constructor(path: string, rotation: string, retention: number, render: (rec: ObsRecord) => string
|
|
42
|
+
constructor(path: string, rotation: string, retention: number, render: (rec: ObsRecord) => string,
|
|
43
|
+
/**
|
|
44
|
+
* A hard ceiling on ONE file, in bytes, on top of time rotation.
|
|
45
|
+
*
|
|
46
|
+
* Daily rotation bounds how many files there are, not how big one can
|
|
47
|
+
* get: a service that starts logging at DEBUG, or loops on an error,
|
|
48
|
+
* can write far more in a day than the fourteen quiet days around it,
|
|
49
|
+
* and nothing stops it before the disk does. With this, the day rolls
|
|
50
|
+
* early instead. Zero disables it, which is what a size-based
|
|
51
|
+
* `rotation` already means.
|
|
52
|
+
*/
|
|
53
|
+
maxBytes?: number);
|
|
32
54
|
private open;
|
|
33
55
|
/** The bucket key for time-based rotation (empty for size-based). */
|
|
34
56
|
private currentPeriod;
|
|
@@ -63,4 +85,4 @@ export declare class RotatingFileWriter {
|
|
|
63
85
|
* own rotation state, so they rename the file out from under each other and
|
|
64
86
|
* the one an operator greps stops advancing. See the slots at the top.
|
|
65
87
|
*/
|
|
66
|
-
export declare function makeFileSink(path: string, format: string, render: (rec: ObsRecord) => string, rotation: string, retention: number): Sink;
|
|
88
|
+
export declare function makeFileSink(path: string, format: string, render: (rec: ObsRecord) => string, rotation: string, retention: number, maxBytes?: number): Sink;
|
package/dist/sinks.js
CHANGED
|
@@ -126,15 +126,28 @@ export class RotatingFileWriter {
|
|
|
126
126
|
rotation;
|
|
127
127
|
retention;
|
|
128
128
|
render;
|
|
129
|
+
maxBytes;
|
|
129
130
|
stream = null;
|
|
130
131
|
bytes = 0;
|
|
131
132
|
periodKey = "";
|
|
132
133
|
disabled = false;
|
|
133
|
-
constructor(path, rotation, retention, render
|
|
134
|
+
constructor(path, rotation, retention, render,
|
|
135
|
+
/**
|
|
136
|
+
* A hard ceiling on ONE file, in bytes, on top of time rotation.
|
|
137
|
+
*
|
|
138
|
+
* Daily rotation bounds how many files there are, not how big one can
|
|
139
|
+
* get: a service that starts logging at DEBUG, or loops on an error,
|
|
140
|
+
* can write far more in a day than the fourteen quiet days around it,
|
|
141
|
+
* and nothing stops it before the disk does. With this, the day rolls
|
|
142
|
+
* early instead. Zero disables it, which is what a size-based
|
|
143
|
+
* `rotation` already means.
|
|
144
|
+
*/
|
|
145
|
+
maxBytes = 0) {
|
|
134
146
|
this.path = path;
|
|
135
147
|
this.rotation = rotation;
|
|
136
148
|
this.retention = retention;
|
|
137
149
|
this.render = render;
|
|
150
|
+
this.maxBytes = maxBytes;
|
|
138
151
|
try {
|
|
139
152
|
mkdirSync(dirname(path), { recursive: true });
|
|
140
153
|
this.open();
|
|
@@ -184,11 +197,18 @@ export class RotatingFileWriter {
|
|
|
184
197
|
const maxBytes = (parseInt(this.rotation, 10) || 100) * 1024 * 1024;
|
|
185
198
|
if (this.bytes > 0 && this.bytes + incoming > maxBytes)
|
|
186
199
|
this.rotateSize();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const now = this.currentPeriod();
|
|
203
|
+
if (this.periodKey && now !== this.periodKey) {
|
|
204
|
+
this.rotateTime(this.periodKey);
|
|
205
|
+
return;
|
|
187
206
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
// The day has not rolled, but this file has outgrown its ceiling. Roll
|
|
208
|
+
// it under today's key; rotatedName() keeps the earlier part rather
|
|
209
|
+
// than renaming over it, so the day survives as .<key>, .<key>.1, ...
|
|
210
|
+
if (this.maxBytes > 0 && this.bytes > 0 && this.bytes + incoming > this.maxBytes) {
|
|
211
|
+
this.rotateTime(now);
|
|
192
212
|
}
|
|
193
213
|
}
|
|
194
214
|
rotateTime(prevKey) {
|
|
@@ -291,11 +311,11 @@ export class RotatingFileWriter {
|
|
|
291
311
|
* own rotation state, so they rename the file out from under each other and
|
|
292
312
|
* the one an operator greps stops advancing. See the slots at the top.
|
|
293
313
|
*/
|
|
294
|
-
export function makeFileSink(path, format, render, rotation, retention) {
|
|
314
|
+
export function makeFileSink(path, format, render, rotation, retention, maxBytes = 0) {
|
|
295
315
|
const open = writers();
|
|
296
316
|
let writer = open.get(path);
|
|
297
317
|
if (!writer) {
|
|
298
|
-
writer = new RotatingFileWriter(path, rotation, retention, render);
|
|
318
|
+
writer = new RotatingFileWriter(path, rotation, retention, render, maxBytes);
|
|
299
319
|
open.set(path, writer);
|
|
300
320
|
}
|
|
301
321
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diveinto/obs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Structured logging, tracing and metrics for Next.js and Node services: one record shape, rotating files, W3C trace context, optional OpenTelemetry export",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|