@diveinto/obs 1.0.2 → 1.0.3

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/config.d.ts CHANGED
@@ -71,11 +71,17 @@ type Resolved = Required<Pick<ObsConfig, "service" | "envPrefix" | "defaultVarDi
71
71
  };
72
72
  export declare function configure(cfg: ObsConfig): void;
73
73
  /**
74
- * The live config. Falling back rather than throwing is deliberate: a log
75
- * line emitted before `configure()` (an import-time warning, say) should
76
- * still come out, named honestly, instead of taking the process down.
74
+ * The live configuration, or a fallback.
75
+ *
76
+ * Falling back rather than throwing is deliberate: a log line emitted before
77
+ * `configure()` (an import-time warning, say) should still come out, named
78
+ * honestly, instead of taking the process down. The fallback is NOT cached,
79
+ * so a later `configure()` still wins, and `isConfigured()` lets the file
80
+ * sinks refuse to invent a path from it.
77
81
  */
78
82
  export declare function obsConfig(): Resolved;
83
+ /** Whether `configure()` has actually run. See `setupLogging()`. */
84
+ export declare function isConfigured(): boolean;
79
85
  /** Test seam: forget the configuration so a test can set a different one. */
80
86
  export declare function resetConfigForTests(): void;
81
87
  /** Read one of this service's own prefixed variables. */
package/dist/config.js CHANGED
@@ -14,7 +14,26 @@
14
14
  * service name is a no-op and a call with a DIFFERENT one is a mistake worth
15
15
  * hearing about.
16
16
  */
17
- let current = null;
17
+ /**
18
+ * The configuration lives on `globalThis`, NOT in a module-level variable.
19
+ *
20
+ * This is not defensiveness, it is a scar. Next can run `instrumentation.ts`
21
+ * in a DIFFERENT module context from the route handlers, so a module-level
22
+ * `let` set by `configure()` reads as unset in half the process. When that
23
+ * happened, the lazy sink initialiser re-ran `setupLogging()` in the second
24
+ * context, got the fallback configuration, tried to open
25
+ * `/var/lib/unconfigured/logs/...`, and self-disabled both file sinks. Every
26
+ * request log went to stdout only and the `.jsonl` and `.log` files silently
27
+ * stopped receiving anything, which is exactly the surface operators grep.
28
+ *
29
+ * `globalThis` is shared across those contexts, which is the same reason
30
+ * @vercel/otel puts its provider there and why `otelEnabled()` reads the
31
+ * environment rather than a flag.
32
+ */
33
+ const SLOT = Symbol.for("diveinto.obs.config");
34
+ function slot() {
35
+ return globalThis;
36
+ }
18
37
  function resolve(cfg) {
19
38
  const service = cfg.service.trim();
20
39
  if (!service)
@@ -35,24 +54,33 @@ function resolve(cfg) {
35
54
  }
36
55
  export function configure(cfg) {
37
56
  const next = resolve(cfg);
38
- if (current && current.service !== next.service) {
57
+ const held = slot()[SLOT];
58
+ if (held && held.service !== next.service) {
39
59
  // Two services in one process is not a thing this library supports, and
40
60
  // the symptom (logs filed under the wrong service) is miserable to chase.
41
- throw new Error(`[obs] already configured as '${current.service}'; cannot reconfigure as '${next.service}'`);
61
+ throw new Error(`[obs] already configured as '${held.service}'; cannot reconfigure as '${next.service}'`);
42
62
  }
43
- current = next;
63
+ slot()[SLOT] = next;
44
64
  }
45
65
  /**
46
- * The live config. Falling back rather than throwing is deliberate: a log
47
- * line emitted before `configure()` (an import-time warning, say) should
48
- * still come out, named honestly, instead of taking the process down.
66
+ * The live configuration, or a fallback.
67
+ *
68
+ * Falling back rather than throwing is deliberate: a log line emitted before
69
+ * `configure()` (an import-time warning, say) should still come out, named
70
+ * honestly, instead of taking the process down. The fallback is NOT cached,
71
+ * so a later `configure()` still wins, and `isConfigured()` lets the file
72
+ * sinks refuse to invent a path from it.
49
73
  */
50
74
  export function obsConfig() {
51
- return current ?? (current = resolve({ service: "unconfigured", envPrefix: "OBS" }));
75
+ return slot()[SLOT] ?? resolve({ service: "unconfigured", envPrefix: "OBS" });
76
+ }
77
+ /** Whether `configure()` has actually run. See `setupLogging()`. */
78
+ export function isConfigured() {
79
+ return slot()[SLOT] !== undefined;
52
80
  }
53
81
  /** Test seam: forget the configuration so a test can set a different one. */
54
82
  export function resetConfigForTests() {
55
- current = null;
83
+ delete slot()[SLOT];
56
84
  }
57
85
  /** Read one of this service's own prefixed variables. */
58
86
  export function envVar(name, env = process.env) {
package/dist/setup.js CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { join } from "node:path";
25
25
  import { logEvent } from "./api.js";
26
- import { obsConfig } from "./config.js";
26
+ import { isConfigured, obsConfig } from "./config.js";
27
27
  import { makeOtelLogSink, otelEnabled } from "./otel.js";
28
28
  import { renderJson, renderPretty } from "./render.js";
29
29
  import { configureSinks, makeFileSink, makeStdoutSink, setSinkInitializer } from "./sinks.js";
@@ -116,8 +116,18 @@ export function setupLogging() {
116
116
  // The two rotating files are self-hosted only. They are format-locked
117
117
  // (<stem>.log = pretty, <stem>.jsonl = json) regardless of LOG_FORMAT, so
118
118
  // each file is always what its name promises.
119
+ //
120
+ // Belt and braces after the globalThis fix: if this ever runs before
121
+ // configure() again, write to stdout and say so, rather than deriving a
122
+ // path from the fallback name. The old behaviour opened
123
+ // /var/lib/unconfigured/logs/, failed, self-disabled both files, and left
124
+ // an operator with a silently empty .jsonl.
119
125
  const serverless = isServerless();
120
- if (!serverless) {
126
+ if (!serverless && !isConfigured()) {
127
+ process.stderr.write("[obs] setupLogging() ran before configure(); writing to stdout only. " +
128
+ "Call configure() or createObs() first, from instrumentation.ts.\n");
129
+ }
130
+ if (!serverless && isConfigured()) {
121
131
  const targets = resolveFileTargets();
122
132
  if (targets.text) {
123
133
  sinks.push(makeFileSink(targets.text.path, "pretty", renderPretty, targets.text.rotation, targets.text.retention));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diveinto/obs",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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,