@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 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 three for `_JSON_`. A
104
- `*_FILE` value is a path, or `1` for the default path, or `0`/`off`.
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
- else {
189
- const now = this.currentPeriod();
190
- if (this.periodKey && now !== this.periodKey)
191
- this.rotateTime(this.periodKey);
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.4",
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,