@storyshelf/observability 0.6.2 → 0.8.0

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
@@ -25,6 +25,46 @@ const storage = createInstrumentedStorage(baseStorage);
25
25
 
26
26
  See the [StoryShelf documentation](https://github.com/GuptaSiddhant/StoryShelf#readme) for configuration details.
27
27
 
28
+ ## Server wiring
29
+
30
+ ```ts
31
+ import { otelLogMixin } from "@storyshelf/observability";
32
+ import { initObservabilityFromEnv } from "@storyshelf/observability/node";
33
+
34
+ // Noop unless OTEL_EXPORTER_OTLP_ENDPOINT is set. Call before createShelfApp.
35
+ const observability = await initObservabilityFromEnv();
36
+
37
+ const app = createShelfApp({
38
+ database,
39
+ storage,
40
+ testing: { runner: captureRunner },
41
+ logger: createShelfLogger({ mixin: otelLogMixin }),
42
+ observability,
43
+ config: { scratchDir: dataDir },
44
+ });
45
+
46
+ await app.lifecycle.setup();
47
+ // on SIGTERM: await app.lifecycle.teardown(); (flushes the SDK)
48
+ ```
49
+
50
+ `storyshelf server init` scaffolds exactly this preamble. Two entrypoints by design: the root is `@opentelemetry/api`
51
+ only (cross-runtime, Deno-safe); `./node` holds the Node SDK lifecycle and
52
+ must never be imported under Deno (`OTEL_DENO=true` uses Deno's native OTEL
53
+ instead).
54
+
55
+ ## What it exports
56
+
57
+ - `initObservabilityFromEnv`, `resolveObservabilityConfig` (`./node` / config) — env-driven SDK lifecycle.
58
+ - `createHttpMiddleware` — `@hono/otel` server spans plus `storyshelf.req_id` / `enduser.id` (mounted by `createShelfApp`).
59
+ - `createInstrumentedDatabase` / `createInstrumentedStorage` — transparent `db.*` / `storage.*` span + metric wrappers (identity, lifecycle, and tables delegate through).
60
+ - `withSpan`, `currentTraceparent`, `injectTraceContext`, `parentContext` — span and W3C propagation helpers.
61
+ - `captureMetrics`, `dbMetrics`, `storageMetrics` — memoized instruments (`capture.job.duration`, `capture.jobs.completed/failed`, operation durations).
62
+ - `otelLogMixin` — pino mixin injecting `trace_id`/`span_id` (logs stay on stdout; no OTLP log exporter by design).
63
+
64
+ Core (`@storyshelf/core`) uses `@opentelemetry/api` directly for pipeline
65
+ spans (`capture.job`, phase spans, `http.client`) and never imports this
66
+ package — that one-way boundary is load-bearing (see ADR 0015 §5).
67
+
28
68
  ## License
29
69
 
30
70
  MIT
package/dist/config.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/config.d.ts
4
3
  /**
5
4
  * Environment-driven observability configuration.
6
5
  *
@@ -33,6 +32,5 @@ interface ObservabilityConfig {
33
32
  * @returns The resolved config; `enabled` is false without an endpoint.
34
33
  */
35
34
  declare function resolveObservabilityConfig(env?: NodeJS.ProcessEnv, serviceVersion?: string): ObservabilityConfig;
36
- //#endregion
37
35
  export { ObservabilityConfig, resolveObservabilityConfig };
38
36
  //# sourceMappingURL=config.d.mts.map
package/dist/config.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/config.ts
4
+ // src/config.ts
5
5
  const DEFAULT_SERVICE_NAME = "storyshelf";
6
6
  const DEFAULT_METRICS_INTERVAL_MS = 6e4;
7
7
  /** Whether the current runtime is Deno (native OTEL owns the SDK there). */
@@ -50,11 +50,14 @@ function resolveObservabilityConfig(env = process.env, serviceVersion) {
50
50
  metricsIntervalMs: parseInterval(env["STORYSHELF_OTEL_METRICS_INTERVAL_MS"], DEFAULT_METRICS_INTERVAL_MS)
51
51
  };
52
52
  }
53
- /** Package version injected at build via __PKG_VERSION__. */
53
+ /**
54
+ * Package version injected at build via the `__PKG_VERSION__` define. Read as a bare identifier
55
+ * (defines don't replace `globalThis` member reads); the `typeof` guard keeps source-condition
56
+ * runs, where the identifier is undeclared, from throwing.
57
+ */
54
58
  function packageVersion() {
55
- return globalThis.__PKG_VERSION__;
59
+ return "0.8.0";
56
60
  }
57
- //#endregion
58
61
  export { resolveObservabilityConfig };
59
62
 
60
63
  //# sourceMappingURL=config.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Environment-driven observability configuration.\n *\n * Standard `OTEL_*` variables win; `STORYSHELF_OTEL_*` covers StoryShelf\n * specifics. When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset the SDK stays a\n * noop (zero overhead, hermetic tests). Deno runtimes are always noop here —\n * Deno's built-in OTEL (`OTEL_DENO=true`) bridges `npm:@opentelemetry/api`\n * natively, so a NodeSDK must never start under Deno.\n */\n\n/** Resolved observability configuration for the Node SDK entrypoint. */\nexport interface ObservabilityConfig {\n /** Whether to start the SDK (false = noop handle). */\n enabled: boolean;\n /** OTLP/HTTP endpoint, e.g. `http://collector:4318`. */\n endpoint?: string;\n /** Extra OTLP headers (`OTEL_EXPORTER_OTLP_HEADERS`, `k=v,k2=v2`). */\n headers?: Record<string, string>;\n /** Resource `service.name`. */\n serviceName: string;\n /** Resource `service.version`. */\n serviceVersion?: string;\n /** Metric export interval in milliseconds. */\n metricsIntervalMs: number;\n}\n\nconst DEFAULT_SERVICE_NAME = \"storyshelf\";\nconst DEFAULT_METRICS_INTERVAL_MS = 60_000;\n\n/** Whether the current runtime is Deno (native OTEL owns the SDK there). */\nfunction isDenoRuntime(env: NodeJS.ProcessEnv): boolean {\n if (env[\"OTEL_DENO\"] === \"true\") {\n return true;\n }\n return (globalThis as { Deno?: unknown }).Deno !== undefined;\n}\n\n/** Parse one `k=v` pair into the headers record. */\nfunction parseHeaderPair(pair: string, headers: Record<string, string>): void {\n const separator = pair.indexOf(\"=\");\n if (separator === -1) {\n return;\n }\n const key = pair.slice(0, separator).trim();\n const value = pair.slice(separator + 1).trim();\n if (key) {\n headers[key] = value;\n }\n}\n\n/** Parse `k=v,k2=v2` header syntax into a record. */\nfunction parseHeaders(raw: string | undefined): Record<string, string> | undefined {\n if (!raw) {\n return undefined;\n }\n const headers: Record<string, string> = {};\n for (const pair of raw.split(\",\")) {\n parseHeaderPair(pair, headers);\n }\n return Object.keys(headers).length === 0 ? undefined : headers;\n}\n\n/** Parse a positive integer env value with a fallback. */\nfunction parseInterval(raw: string | undefined, fallback: number): number {\n const parsed = Number(raw);\n if (!raw || !Number.isFinite(parsed) || parsed <= 0) {\n return fallback;\n }\n return Math.trunc(parsed);\n}\n\n/**\n * Resolve the observability configuration from the environment.\n *\n * @param env - Environment record (defaults to `process.env`).\n * @param serviceVersion - Version for the resource (defaults to `__PKG_VERSION__`).\n * @returns The resolved config; `enabled` is false without an endpoint.\n */\nexport function resolveObservabilityConfig(\n env: NodeJS.ProcessEnv = process.env,\n serviceVersion?: string,\n): ObservabilityConfig {\n const endpoint = env[\"OTEL_EXPORTER_OTLP_ENDPOINT\"];\n const sdkDisabled = env[\"OTEL_SDK_DISABLED\"] === \"true\";\n const explicit = env[\"STORYSHELF_OTEL_ENABLED\"];\n const enabled =\n !sdkDisabled && !isDenoRuntime(env) && (explicit ? explicit === \"true\" : Boolean(endpoint));\n return {\n enabled,\n endpoint,\n headers: parseHeaders(env[\"OTEL_EXPORTER_OTLP_HEADERS\"]),\n serviceName: env[\"OTEL_SERVICE_NAME\"] ?? DEFAULT_SERVICE_NAME,\n serviceVersion: serviceVersion ?? packageVersion(),\n metricsIntervalMs: parseInterval(\n env[\"STORYSHELF_OTEL_METRICS_INTERVAL_MS\"],\n DEFAULT_METRICS_INTERVAL_MS,\n ),\n };\n}\n\n/** Package version injected at build via __PKG_VERSION__. */\nfunction packageVersion(): string | undefined {\n return (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__;\n}\n"],"mappings":";;;;AA0BA,MAAM,uBAAuB;AAC7B,MAAM,8BAA8B;;AAGpC,SAAS,cAAc,KAAiC;CACtD,IAAI,IAAI,iBAAiB,QACvB,OAAO;CAET,OAAQ,WAAkC,SAAS,KAAA;AACrD;;AAGA,SAAS,gBAAgB,MAAc,SAAuC;CAC5E,MAAM,YAAY,KAAK,QAAQ,GAAG;CAClC,IAAI,cAAc,IAChB;CAEF,MAAM,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC,CAAC,KAAK;CAC1C,MAAM,QAAQ,KAAK,MAAM,YAAY,CAAC,CAAC,CAAC,KAAK;CAC7C,IAAI,KACF,QAAQ,OAAO;AAEnB;;AAGA,SAAS,aAAa,KAA6D;CACjF,IAAI,CAAC,KACH;CAEF,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,QAAQ,IAAI,MAAM,GAAG,GAC9B,gBAAgB,MAAM,OAAO;CAE/B,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW,IAAI,KAAA,IAAY;AACzD;;AAGA,SAAS,cAAc,KAAyB,UAA0B;CACxE,MAAM,SAAS,OAAO,GAAG;CACzB,IAAI,CAAC,OAAO,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GAChD,OAAO;CAET,OAAO,KAAK,MAAM,MAAM;AAC1B;;;;;;;;AASA,SAAgB,2BACd,MAAyB,QAAQ,KACjC,gBACqB;CACrB,MAAM,WAAW,IAAI;CACrB,MAAM,cAAc,IAAI,yBAAyB;CACjD,MAAM,WAAW,IAAI;CAGrB,OAAO;EACL,SAFA,CAAC,eAAe,CAAC,cAAc,GAAG,MAAM,WAAW,aAAa,SAAS,QAAQ,QAAQ;EAGzF;EACA,SAAS,aAAa,IAAI,6BAA6B;EACvD,aAAa,IAAI,wBAAwB;EACzC,gBAAgB,kBAAkB,eAAe;EACjD,mBAAmB,cACjB,IAAI,wCACJ,2BACF;CACF;AACF;;AAGA,SAAS,iBAAqC;CAC5C,OAAQ,WAAuD;AACjE"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Environment-driven observability configuration.\n *\n * Standard `OTEL_*` variables win; `STORYSHELF_OTEL_*` covers StoryShelf\n * specifics. When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset the SDK stays a\n * noop (zero overhead, hermetic tests). Deno runtimes are always noop here —\n * Deno's built-in OTEL (`OTEL_DENO=true`) bridges `npm:@opentelemetry/api`\n * natively, so a NodeSDK must never start under Deno.\n */\n\n/** Resolved observability configuration for the Node SDK entrypoint. */\nexport interface ObservabilityConfig {\n /** Whether to start the SDK (false = noop handle). */\n enabled: boolean;\n /** OTLP/HTTP endpoint, e.g. `http://collector:4318`. */\n endpoint?: string;\n /** Extra OTLP headers (`OTEL_EXPORTER_OTLP_HEADERS`, `k=v,k2=v2`). */\n headers?: Record<string, string>;\n /** Resource `service.name`. */\n serviceName: string;\n /** Resource `service.version`. */\n serviceVersion?: string;\n /** Metric export interval in milliseconds. */\n metricsIntervalMs: number;\n}\n\nconst DEFAULT_SERVICE_NAME = \"storyshelf\";\nconst DEFAULT_METRICS_INTERVAL_MS = 60_000;\n\n/** Whether the current runtime is Deno (native OTEL owns the SDK there). */\nfunction isDenoRuntime(env: NodeJS.ProcessEnv): boolean {\n if (env[\"OTEL_DENO\"] === \"true\") {\n return true;\n }\n return (globalThis as { Deno?: unknown }).Deno !== undefined;\n}\n\n/** Parse one `k=v` pair into the headers record. */\nfunction parseHeaderPair(pair: string, headers: Record<string, string>): void {\n const separator = pair.indexOf(\"=\");\n if (separator === -1) {\n return;\n }\n const key = pair.slice(0, separator).trim();\n const value = pair.slice(separator + 1).trim();\n if (key) {\n headers[key] = value;\n }\n}\n\n/** Parse `k=v,k2=v2` header syntax into a record. */\nfunction parseHeaders(raw: string | undefined): Record<string, string> | undefined {\n if (!raw) {\n return undefined;\n }\n const headers: Record<string, string> = {};\n for (const pair of raw.split(\",\")) {\n parseHeaderPair(pair, headers);\n }\n return Object.keys(headers).length === 0 ? undefined : headers;\n}\n\n/** Parse a positive integer env value with a fallback. */\nfunction parseInterval(raw: string | undefined, fallback: number): number {\n const parsed = Number(raw);\n if (!raw || !Number.isFinite(parsed) || parsed <= 0) {\n return fallback;\n }\n return Math.trunc(parsed);\n}\n\n/**\n * Resolve the observability configuration from the environment.\n *\n * @param env - Environment record (defaults to `process.env`).\n * @param serviceVersion - Version for the resource (defaults to `__PKG_VERSION__`).\n * @returns The resolved config; `enabled` is false without an endpoint.\n */\nexport function resolveObservabilityConfig(\n env: NodeJS.ProcessEnv = process.env,\n serviceVersion?: string,\n): ObservabilityConfig {\n const endpoint = env[\"OTEL_EXPORTER_OTLP_ENDPOINT\"];\n const sdkDisabled = env[\"OTEL_SDK_DISABLED\"] === \"true\";\n const explicit = env[\"STORYSHELF_OTEL_ENABLED\"];\n const enabled =\n !sdkDisabled && !isDenoRuntime(env) && (explicit ? explicit === \"true\" : Boolean(endpoint));\n return {\n enabled,\n endpoint,\n headers: parseHeaders(env[\"OTEL_EXPORTER_OTLP_HEADERS\"]),\n serviceName: env[\"OTEL_SERVICE_NAME\"] ?? DEFAULT_SERVICE_NAME,\n serviceVersion: serviceVersion ?? packageVersion(),\n metricsIntervalMs: parseInterval(\n env[\"STORYSHELF_OTEL_METRICS_INTERVAL_MS\"],\n DEFAULT_METRICS_INTERVAL_MS,\n ),\n };\n}\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Package version injected at build via the `__PKG_VERSION__` define. Read as a bare identifier\n * (defines don't replace `globalThis` member reads); the `typeof` guard keeps source-condition\n * runs, where the identifier is undeclared, from throwing.\n */\nfunction packageVersion(): string | undefined {\n return typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : undefined;\n}\n"],"mappings":";;;;AA0BA,MAAM,uBAAuB;AAC7B,MAAM,8BAA8B;;AAGpC,SAAS,cAAc,KAAiC;CACtD,IAAI,IAAI,iBAAiB,QACvB,OAAO;CAET,OAAQ,WAAkC,SAAS,KAAA;AACrD;;AAGA,SAAS,gBAAgB,MAAc,SAAuC;CAC5E,MAAM,YAAY,KAAK,QAAQ,GAAG;CAClC,IAAI,cAAc,IAChB;CAEF,MAAM,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC,CAAC,KAAK;CAC1C,MAAM,QAAQ,KAAK,MAAM,YAAY,CAAC,CAAC,CAAC,KAAK;CAC7C,IAAI,KACF,QAAQ,OAAO;AAEnB;;AAGA,SAAS,aAAa,KAA6D;CACjF,IAAI,CAAC,KACH;CAEF,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,QAAQ,IAAI,MAAM,GAAG,GAC9B,gBAAgB,MAAM,OAAO;CAE/B,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW,IAAI,KAAA,IAAY;AACzD;;AAGA,SAAS,cAAc,KAAyB,UAA0B;CACxE,MAAM,SAAS,OAAO,GAAG;CACzB,IAAI,CAAC,OAAO,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GAChD,OAAO;CAET,OAAO,KAAK,MAAM,MAAM;AAC1B;;;;;;;;AASA,SAAgB,2BACd,MAAyB,QAAQ,KACjC,gBACqB;CACrB,MAAM,WAAW,IAAI;CACrB,MAAM,cAAc,IAAI,yBAAyB;CACjD,MAAM,WAAW,IAAI;CAGrB,OAAO;EACL,SAFA,CAAC,eAAe,CAAC,cAAc,GAAG,MAAM,WAAW,aAAa,SAAS,QAAQ,QAAQ;EAGzF;EACA,SAAS,aAAa,IAAI,6BAA6B;EACvD,aAAa,IAAI,wBAAwB;EACzC,gBAAgB,kBAAkB,eAAe;EACjD,mBAAmB,cACjB,IAAI,wCACJ,2BACF;CACF;AACF;;;;;;AASA,SAAS,iBAAqC;CAC5C,OAAA;AACF"}
package/dist/index.mjs CHANGED
@@ -9,16 +9,4 @@ import { createHttpMiddleware } from "./middleware.mjs";
9
9
  import { otelLogMixin } from "./logs.mjs";
10
10
  import { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext } from "./propagate.mjs";
11
11
  import { resolveObservabilityConfig } from "./config.mjs";
12
- //#region src/index.ts
13
- /**
14
- * StoryShelf observability: runtime-agnostic OTEL instrumentation.
15
- *
16
- * This entrypoint is cross-runtime (`@opentelemetry/api` only — Node SDK or
17
- * Deno's native bridge supplies the provider). The Node SDK lifecycle
18
- * (`initObservabilityFromEnv`) lives under `@storyshelf/observability/node`
19
- * and must never be imported here.
20
- */
21
- //#endregion
22
12
  export { TRACEPARENT_HEADER, captureMetrics, createHttpMiddleware, createInstrumentedDatabase, createInstrumentedStorage, currentTraceparent, dbMetrics, injectTraceContext, otelLogMixin, parentContext, resolveObservabilityConfig, storageMetrics, withSpan };
23
-
24
- //# sourceMappingURL=index.mjs.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
4
- //#region src/instrument-db.d.ts
5
4
  /**
6
5
  * Wrap a database adapter with per-operation spans and metrics.
7
6
  *
@@ -9,6 +8,5 @@ import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
9
8
  * @returns A transparent adapter emitting `db.*` telemetry.
10
9
  */
11
10
  declare function createInstrumentedDatabase(db: DatabaseAdapter): DatabaseAdapter;
12
- //#endregion
13
11
  export { createInstrumentedDatabase };
14
12
  //# sourceMappingURL=instrument-db.d.mts.map
@@ -5,7 +5,7 @@ import { dbMetrics } from "./metrics.mjs";
5
5
  import { withSpan } from "./tracing.mjs";
6
6
  import { addTiming } from "@storyshelf/core/utils";
7
7
  import { getTableName } from "drizzle-orm";
8
- //#region src/instrument-db.ts
8
+ // src/instrument-db.ts
9
9
  /**
10
10
  * Wrap a database adapter with per-operation spans and metrics.
11
11
  *
@@ -17,8 +17,11 @@ function createInstrumentedDatabase(db) {
17
17
  }
18
18
  var InstrumentedDatabase = class {
19
19
  inner;
20
+ transact;
20
21
  constructor(inner) {
21
22
  this.inner = inner;
23
+ const transact = inner.transact?.bind(inner);
24
+ this.transact = transact ? async (fn) => await trackQuery("db.transact", async () => await transact(fn)) : void 0;
22
25
  }
23
26
  get metadata() {
24
27
  return this.inner.metadata;
@@ -26,6 +29,9 @@ var InstrumentedDatabase = class {
26
29
  get lifecycle() {
27
30
  return this.inner.lifecycle;
28
31
  }
32
+ get buildReferenceLifecycle() {
33
+ return this.inner.buildReferenceLifecycle;
34
+ }
29
35
  get tables() {
30
36
  return this.inner.tables;
31
37
  }
@@ -38,6 +44,9 @@ var InstrumentedDatabase = class {
38
44
  async update(table, id, values) {
39
45
  return await track("db.update", table, async () => await this.inner.update(table, id, values));
40
46
  }
47
+ async updateWhere(table, id, values, where) {
48
+ return await track("db.updateWhere", table, async () => await this.inner.updateWhere(table, id, values, where));
49
+ }
41
50
  async get(table, id) {
42
51
  return await track("db.get", table, async () => await this.inner.get(table, id));
43
52
  }
@@ -82,7 +91,6 @@ async function trackQuery(operation, fn) {
82
91
  dbMetrics().operationDuration.record(durationMs, { "db.operation": operation });
83
92
  }
84
93
  }
85
- //#endregion
86
94
  export { createInstrumentedDatabase };
87
95
 
88
96
  //# sourceMappingURL=instrument-db.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrument-db.mjs","names":[],"sources":["../src/instrument-db.ts"],"sourcesContent":["/**\n * Database adapter wrapper: one span + duration metric per operation.\n *\n * Span names are `db.<operation>` with the table as an attribute (never in\n * the name). Adapter identity (`metadata`, `lifecycle`, `tables`) and the\n * host-bound logger delegate straight through, so health/setup behave\n * identically with or without observability.\n */\nimport type { DatabaseAdapter, ListOptions, Tables } from \"@storyshelf/core/adapter/database\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { addTiming } from \"@storyshelf/core/utils\";\nimport { getTableName, type SQL, type Table } from \"drizzle-orm\";\nimport { dbMetrics } from \"./metrics.ts\";\nimport { withSpan } from \"./tracing.ts\";\n\n/**\n * Wrap a database adapter with per-operation spans and metrics.\n *\n * @param db - The adapter to wrap (never mutated).\n * @returns A transparent adapter emitting `db.*` telemetry.\n */\nexport function createInstrumentedDatabase(db: DatabaseAdapter): DatabaseAdapter {\n return new InstrumentedDatabase(db);\n}\n\nclass InstrumentedDatabase implements DatabaseAdapter {\n constructor(private readonly inner: DatabaseAdapter) {}\n\n get metadata(): DatabaseAdapter[\"metadata\"] {\n return this.inner.metadata;\n }\n\n get lifecycle(): DatabaseAdapter[\"lifecycle\"] {\n return this.inner.lifecycle;\n }\n\n get tables(): Tables {\n return this.inner.tables;\n }\n\n setLogger(logger: Logger): void {\n this.inner.setLogger?.(logger);\n }\n\n async insert<T extends Table>(table: T, values: T[\"$inferInsert\"]): Promise<T[\"$inferSelect\"]> {\n return await track(\"db.insert\", table, async () => await this.inner.insert(table, values));\n }\n\n async update<T extends Table>(\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n ): Promise<T[\"$inferSelect\"]> {\n return await track(\"db.update\", table, async () => await this.inner.update(table, id, values));\n }\n\n async get<T extends Table>(table: T, id: string): Promise<T[\"$inferSelect\"] | null> {\n return await track(\"db.get\", table, async () => await this.inner.get(table, id));\n }\n\n async remove(table: Table, id: string): Promise<void> {\n await track(\"db.remove\", table, async () => {\n await this.inner.remove(table, id);\n });\n }\n\n async list<T extends Table>(table: T, opts?: ListOptions): Promise<T[\"$inferSelect\"][]> {\n return await track(\"db.list\", table, async () => await this.inner.list(table, opts));\n }\n\n async count(table: Table, where?: SQL): Promise<number> {\n return await track(\"db.count\", table, async () => await this.inner.count(table, where));\n }\n\n async all<T>(query: SQL): Promise<T[]> {\n return await trackQuery(\"db.all\", async () => await this.inner.all<T>(query));\n }\n}\n\n/** Run a table operation inside a span with a duration measurement. */\nasync function track<T>(operation: string, table: Table, fn: () => Promise<T>): Promise<T> {\n const tableName = getTableName(table);\n const start = performance.now();\n try {\n return await withSpan(operation, fn, { \"db.table\": tableName });\n } finally {\n const durationMs = performance.now() - start;\n addTiming(\"db\", durationMs);\n dbMetrics().operationDuration.record(durationMs, {\n \"db.operation\": operation,\n \"db.table\": tableName,\n });\n }\n}\n\n/** Run a table-less query inside a span with a duration measurement. */\nasync function trackQuery<T>(operation: string, fn: () => Promise<T>): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(operation, fn);\n } finally {\n const durationMs = performance.now() - start;\n addTiming(\"db\", durationMs);\n dbMetrics().operationDuration.record(durationMs, {\n \"db.operation\": operation,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBA,SAAgB,2BAA2B,IAAsC;CAC/E,OAAO,IAAI,qBAAqB,EAAE;AACpC;AAEA,IAAM,uBAAN,MAAsD;CACvB;CAA7B,YAAY,OAAyC;EAAxB,KAAA,QAAA;CAAyB;CAEtD,IAAI,WAAwC;EAC1C,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,YAA0C;EAC5C,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK,MAAM;CACpB;CAEA,UAAU,QAAsB;EAC9B,KAAK,MAAM,YAAY,MAAM;CAC/B;CAEA,MAAM,OAAwB,OAAU,QAAuD;EAC7F,OAAO,MAAM,MAAM,aAAa,OAAO,YAAY,MAAM,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC;CAC3F;CAEA,MAAM,OACJ,OACA,IACA,QAC4B;EAC5B,OAAO,MAAM,MAAM,aAAa,OAAO,YAAY,MAAM,KAAK,MAAM,OAAO,OAAO,IAAI,MAAM,CAAC;CAC/F;CAEA,MAAM,IAAqB,OAAU,IAA+C;EAClF,OAAO,MAAM,MAAM,UAAU,OAAO,YAAY,MAAM,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC;CACjF;CAEA,MAAM,OAAO,OAAc,IAA2B;EACpD,MAAM,MAAM,aAAa,OAAO,YAAY;GAC1C,MAAM,KAAK,MAAM,OAAO,OAAO,EAAE;EACnC,CAAC;CACH;CAEA,MAAM,KAAsB,OAAU,MAAkD;EACtF,OAAO,MAAM,MAAM,WAAW,OAAO,YAAY,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,CAAC;CACrF;CAEA,MAAM,MAAM,OAAc,OAA8B;EACtD,OAAO,MAAM,MAAM,YAAY,OAAO,YAAY,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,CAAC;CACxF;CAEA,MAAM,IAAO,OAA0B;EACrC,OAAO,MAAM,WAAW,UAAU,YAAY,MAAM,KAAK,MAAM,IAAO,KAAK,CAAC;CAC9E;AACF;;AAGA,eAAe,MAAS,WAAmB,OAAc,IAAkC;CACzF,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,OAAO,MAAM,SAAS,WAAW,IAAI,EAAE,YAAY,UAAU,CAAC;CAChE,UAAU;EACR,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,UAAU,MAAM,UAAU;EAC1B,UAAU,CAAC,CAAC,kBAAkB,OAAO,YAAY;GAC/C,gBAAgB;GAChB,YAAY;EACd,CAAC;CACH;AACF;;AAGA,eAAe,WAAc,WAAmB,IAAkC;CAChF,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,OAAO,MAAM,SAAS,WAAW,EAAE;CACrC,UAAU;EACR,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,UAAU,MAAM,UAAU;EAC1B,UAAU,CAAC,CAAC,kBAAkB,OAAO,YAAY,EAC/C,gBAAgB,UAClB,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"instrument-db.mjs","names":[],"sources":["../src/instrument-db.ts"],"sourcesContent":["/**\n * Database adapter wrapper: one span + duration metric per operation.\n *\n * Span names are `db.<operation>` with the table as an attribute (never in\n * the name). Adapter identity (`metadata`, `lifecycle`, `tables`) and the\n * host-bound logger delegate straight through, so health/setup behave\n * identically with or without observability.\n */\nimport type {\n DatabaseAdapter,\n ListOptions,\n Tables,\n TxStore,\n} from \"@storyshelf/core/adapter/database\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { addTiming } from \"@storyshelf/core/utils\";\nimport { getTableName, type SQL, type Table } from \"drizzle-orm\";\nimport { dbMetrics } from \"./metrics.ts\";\nimport { withSpan } from \"./tracing.ts\";\n\n/**\n * Wrap a database adapter with per-operation spans and metrics.\n *\n * @param db - The adapter to wrap (never mutated).\n * @returns A transparent adapter emitting `db.*` telemetry.\n */\nexport function createInstrumentedDatabase(db: DatabaseAdapter): DatabaseAdapter {\n return new InstrumentedDatabase(db);\n}\n\nclass InstrumentedDatabase implements DatabaseAdapter {\n private readonly inner: DatabaseAdapter;\n readonly transact: DatabaseAdapter[\"transact\"];\n\n constructor(inner: DatabaseAdapter) {\n this.inner = inner;\n const transact = inner.transact?.bind(inner);\n this.transact = transact\n ? async <Result>(fn: (tx: TxStore) => Promise<Result>): Promise<Result> =>\n await trackQuery(\"db.transact\", async () => await transact(fn))\n : undefined;\n }\n\n get metadata(): DatabaseAdapter[\"metadata\"] {\n return this.inner.metadata;\n }\n\n get lifecycle(): DatabaseAdapter[\"lifecycle\"] {\n return this.inner.lifecycle;\n }\n\n get buildReferenceLifecycle(): DatabaseAdapter[\"buildReferenceLifecycle\"] {\n return this.inner.buildReferenceLifecycle;\n }\n\n get tables(): Tables {\n return this.inner.tables;\n }\n\n setLogger(logger: Logger): void {\n this.inner.setLogger?.(logger);\n }\n\n async insert<T extends Table>(table: T, values: T[\"$inferInsert\"]): Promise<T[\"$inferSelect\"]> {\n return await track(\"db.insert\", table, async () => await this.inner.insert(table, values));\n }\n\n async update<T extends Table>(\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n ): Promise<T[\"$inferSelect\"]> {\n return await track(\"db.update\", table, async () => await this.inner.update(table, id, values));\n }\n\n async updateWhere<T extends Table>(\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n where: SQL,\n ): Promise<T[\"$inferSelect\"] | null> {\n return await track(\n \"db.updateWhere\",\n table,\n async () => await this.inner.updateWhere(table, id, values, where),\n );\n }\n\n async get<T extends Table>(table: T, id: string): Promise<T[\"$inferSelect\"] | null> {\n return await track(\"db.get\", table, async () => await this.inner.get(table, id));\n }\n\n async remove(table: Table, id: string): Promise<void> {\n await track(\"db.remove\", table, async () => {\n await this.inner.remove(table, id);\n });\n }\n\n async list<T extends Table>(table: T, opts?: ListOptions): Promise<T[\"$inferSelect\"][]> {\n return await track(\"db.list\", table, async () => await this.inner.list(table, opts));\n }\n\n async count(table: Table, where?: SQL): Promise<number> {\n return await track(\"db.count\", table, async () => await this.inner.count(table, where));\n }\n\n async all<T>(query: SQL): Promise<T[]> {\n return await trackQuery(\"db.all\", async () => await this.inner.all<T>(query));\n }\n}\n\n/** Run a table operation inside a span with a duration measurement. */\nasync function track<T>(operation: string, table: Table, fn: () => Promise<T>): Promise<T> {\n const tableName = getTableName(table);\n const start = performance.now();\n try {\n return await withSpan(operation, fn, { \"db.table\": tableName });\n } finally {\n const durationMs = performance.now() - start;\n addTiming(\"db\", durationMs);\n dbMetrics().operationDuration.record(durationMs, {\n \"db.operation\": operation,\n \"db.table\": tableName,\n });\n }\n}\n\n/** Run a table-less query inside a span with a duration measurement. */\nasync function trackQuery<T>(operation: string, fn: () => Promise<T>): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(operation, fn);\n } finally {\n const durationMs = performance.now() - start;\n addTiming(\"db\", durationMs);\n dbMetrics().operationDuration.record(durationMs, {\n \"db.operation\": operation,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA0BA,SAAgB,2BAA2B,IAAsC;CAC/E,OAAO,IAAI,qBAAqB,EAAE;AACpC;AAEA,IAAM,uBAAN,MAAsD;CACpD;CACA;CAEA,YAAY,OAAwB;EAClC,KAAK,QAAQ;EACb,MAAM,WAAW,MAAM,UAAU,KAAK,KAAK;EAC3C,KAAK,WAAW,WACZ,OAAe,OACb,MAAM,WAAW,eAAe,YAAY,MAAM,SAAS,EAAE,CAAC,IAChE,KAAA;CACN;CAEA,IAAI,WAAwC;EAC1C,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,YAA0C;EAC5C,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,0BAAsE;EACxE,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK,MAAM;CACpB;CAEA,UAAU,QAAsB;EAC9B,KAAK,MAAM,YAAY,MAAM;CAC/B;CAEA,MAAM,OAAwB,OAAU,QAAuD;EAC7F,OAAO,MAAM,MAAM,aAAa,OAAO,YAAY,MAAM,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC;CAC3F;CAEA,MAAM,OACJ,OACA,IACA,QAC4B;EAC5B,OAAO,MAAM,MAAM,aAAa,OAAO,YAAY,MAAM,KAAK,MAAM,OAAO,OAAO,IAAI,MAAM,CAAC;CAC/F;CAEA,MAAM,YACJ,OACA,IACA,QACA,OACmC;EACnC,OAAO,MAAM,MACX,kBACA,OACA,YAAY,MAAM,KAAK,MAAM,YAAY,OAAO,IAAI,QAAQ,KAAK,CACnE;CACF;CAEA,MAAM,IAAqB,OAAU,IAA+C;EAClF,OAAO,MAAM,MAAM,UAAU,OAAO,YAAY,MAAM,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC;CACjF;CAEA,MAAM,OAAO,OAAc,IAA2B;EACpD,MAAM,MAAM,aAAa,OAAO,YAAY;GAC1C,MAAM,KAAK,MAAM,OAAO,OAAO,EAAE;EACnC,CAAC;CACH;CAEA,MAAM,KAAsB,OAAU,MAAkD;EACtF,OAAO,MAAM,MAAM,WAAW,OAAO,YAAY,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,CAAC;CACrF;CAEA,MAAM,MAAM,OAAc,OAA8B;EACtD,OAAO,MAAM,MAAM,YAAY,OAAO,YAAY,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,CAAC;CACxF;CAEA,MAAM,IAAO,OAA0B;EACrC,OAAO,MAAM,WAAW,UAAU,YAAY,MAAM,KAAK,MAAM,IAAO,KAAK,CAAC;CAC9E;AACF;;AAGA,eAAe,MAAS,WAAmB,OAAc,IAAkC;CACzF,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,OAAO,MAAM,SAAS,WAAW,IAAI,EAAE,YAAY,UAAU,CAAC;CAChE,UAAU;EACR,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,UAAU,MAAM,UAAU;EAC1B,UAAU,CAAC,CAAC,kBAAkB,OAAO,YAAY;GAC/C,gBAAgB;GAChB,YAAY;EACd,CAAC;CACH;AACF;;AAGA,eAAe,WAAc,WAAmB,IAAkC;CAChF,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,OAAO,MAAM,SAAS,WAAW,EAAE;CACrC,UAAU;EACR,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,UAAU,MAAM,UAAU;EAC1B,UAAU,CAAC,CAAC,kBAAkB,OAAO,YAAY,EAC/C,gBAAgB,UAClB,CAAC;CACH;AACF"}
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { StorageAdapter } from "@storyshelf/core/adapter/storage";
4
- //#region src/instrument-storage.d.ts
5
4
  /**
6
5
  * Wrap a storage adapter with per-operation spans and metrics.
7
6
  *
@@ -9,6 +8,5 @@ import { StorageAdapter } from "@storyshelf/core/adapter/storage";
9
8
  * @returns A transparent adapter emitting `storage.*` telemetry.
10
9
  */
11
10
  declare function createInstrumentedStorage(storage: StorageAdapter): StorageAdapter;
12
- //#endregion
13
11
  export { createInstrumentedStorage };
14
12
  //# sourceMappingURL=instrument-storage.d.mts.map
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { storageMetrics } from "./metrics.mjs";
5
5
  import { withSpan } from "./tracing.mjs";
6
6
  import { addTiming } from "@storyshelf/core/utils";
7
- //#region src/instrument-storage.ts
7
+ // src/instrument-storage.ts
8
8
  /**
9
9
  * Wrap a storage adapter with per-operation spans and metrics.
10
10
  *
@@ -67,7 +67,6 @@ async function track(operation, path, fn) {
67
67
  storageMetrics().operationDuration.record(durationMs, { "storage.operation": operation });
68
68
  }
69
69
  }
70
- //#endregion
71
70
  export { createInstrumentedStorage };
72
71
 
73
72
  //# sourceMappingURL=instrument-storage.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrument-storage.mjs","names":[],"sources":["../src/instrument-storage.ts"],"sourcesContent":["/**\n * Storage adapter wrapper: one span + duration metric per operation.\n *\n * Span names are `storage.<operation>` with the path as an attribute.\n * Metrics carry the operation only (paths are high-cardinality and stay\n * out of metric attributes). Identity, lifecycle, and logger delegate\n * straight through.\n */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { addTiming } from \"@storyshelf/core/utils\";\nimport type { Buffer } from \"node:buffer\";\nimport type { Readable } from \"node:stream\";\nimport { storageMetrics } from \"./metrics.ts\";\nimport { withSpan } from \"./tracing.ts\";\n\n/**\n * Wrap a storage adapter with per-operation spans and metrics.\n *\n * @param storage - The adapter to wrap (never mutated).\n * @returns A transparent adapter emitting `storage.*` telemetry.\n */\nexport function createInstrumentedStorage(storage: StorageAdapter): StorageAdapter {\n return new InstrumentedStorage(storage);\n}\n\nclass InstrumentedStorage implements StorageAdapter {\n constructor(private readonly inner: StorageAdapter) {}\n\n get metadata(): StorageAdapter[\"metadata\"] {\n return this.inner.metadata;\n }\n\n get lifecycle(): StorageAdapter[\"lifecycle\"] {\n return this.inner.lifecycle;\n }\n\n setLogger(logger: Logger): void {\n this.inner.setLogger?.(logger);\n }\n\n async read(path: string): Promise<Buffer> {\n return await track(\"storage.read\", path, async () => await this.inner.read(path));\n }\n\n async write(path: string, data: Buffer): Promise<void> {\n await track(\"storage.write\", path, async () => {\n await this.inner.write(path, data);\n });\n }\n\n async delete(path: string): Promise<void> {\n await track(\"storage.delete\", path, async () => {\n await this.inner.delete(path);\n });\n }\n\n async exists(path: string): Promise<boolean> {\n return await track(\"storage.exists\", path, async () => await this.inner.exists(path));\n }\n\n async list(prefix: string): Promise<string[]> {\n return await track(\"storage.list\", prefix, async () => await this.inner.list(prefix));\n }\n\n async writeStream(path: string, stream: Readable): Promise<void> {\n await track(\"storage.writeStream\", path, async () => {\n await this.inner.writeStream(path, stream);\n });\n }\n\n async readStream(path: string): Promise<Readable> {\n return await track(\"storage.readStream\", path, async () => await this.inner.readStream(path));\n }\n}\n\n/** Run a storage operation inside a span with a duration measurement. */\nasync function track<T>(operation: string, path: string, fn: () => Promise<T>): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(operation, fn, { \"storage.path\": path });\n } finally {\n const durationMs = performance.now() - start;\n addTiming(\"storage\", durationMs);\n storageMetrics().operationDuration.record(durationMs, {\n \"storage.operation\": operation,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAgB,0BAA0B,SAAyC;CACjF,OAAO,IAAI,oBAAoB,OAAO;AACxC;AAEA,IAAM,sBAAN,MAAoD;CACrB;CAA7B,YAAY,OAAwC;EAAvB,KAAA,QAAA;CAAwB;CAErD,IAAI,WAAuC;EACzC,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,YAAyC;EAC3C,OAAO,KAAK,MAAM;CACpB;CAEA,UAAU,QAAsB;EAC9B,KAAK,MAAM,YAAY,MAAM;CAC/B;CAEA,MAAM,KAAK,MAA+B;EACxC,OAAO,MAAM,MAAM,gBAAgB,MAAM,YAAY,MAAM,KAAK,MAAM,KAAK,IAAI,CAAC;CAClF;CAEA,MAAM,MAAM,MAAc,MAA6B;EACrD,MAAM,MAAM,iBAAiB,MAAM,YAAY;GAC7C,MAAM,KAAK,MAAM,MAAM,MAAM,IAAI;EACnC,CAAC;CACH;CAEA,MAAM,OAAO,MAA6B;EACxC,MAAM,MAAM,kBAAkB,MAAM,YAAY;GAC9C,MAAM,KAAK,MAAM,OAAO,IAAI;EAC9B,CAAC;CACH;CAEA,MAAM,OAAO,MAAgC;EAC3C,OAAO,MAAM,MAAM,kBAAkB,MAAM,YAAY,MAAM,KAAK,MAAM,OAAO,IAAI,CAAC;CACtF;CAEA,MAAM,KAAK,QAAmC;EAC5C,OAAO,MAAM,MAAM,gBAAgB,QAAQ,YAAY,MAAM,KAAK,MAAM,KAAK,MAAM,CAAC;CACtF;CAEA,MAAM,YAAY,MAAc,QAAiC;EAC/D,MAAM,MAAM,uBAAuB,MAAM,YAAY;GACnD,MAAM,KAAK,MAAM,YAAY,MAAM,MAAM;EAC3C,CAAC;CACH;CAEA,MAAM,WAAW,MAAiC;EAChD,OAAO,MAAM,MAAM,sBAAsB,MAAM,YAAY,MAAM,KAAK,MAAM,WAAW,IAAI,CAAC;CAC9F;AACF;;AAGA,eAAe,MAAS,WAAmB,MAAc,IAAkC;CACzF,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,OAAO,MAAM,SAAS,WAAW,IAAI,EAAE,gBAAgB,KAAK,CAAC;CAC/D,UAAU;EACR,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,UAAU,WAAW,UAAU;EAC/B,eAAe,CAAC,CAAC,kBAAkB,OAAO,YAAY,EACpD,qBAAqB,UACvB,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"instrument-storage.mjs","names":[],"sources":["../src/instrument-storage.ts"],"sourcesContent":["/**\n * Storage adapter wrapper: one span + duration metric per operation.\n *\n * Span names are `storage.<operation>` with the path as an attribute.\n * Metrics carry the operation only (paths are high-cardinality and stay\n * out of metric attributes). Identity, lifecycle, and logger delegate\n * straight through.\n */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { addTiming } from \"@storyshelf/core/utils\";\nimport type { Buffer } from \"node:buffer\";\nimport type { Readable } from \"node:stream\";\nimport { storageMetrics } from \"./metrics.ts\";\nimport { withSpan } from \"./tracing.ts\";\n\n/**\n * Wrap a storage adapter with per-operation spans and metrics.\n *\n * @param storage - The adapter to wrap (never mutated).\n * @returns A transparent adapter emitting `storage.*` telemetry.\n */\nexport function createInstrumentedStorage(storage: StorageAdapter): StorageAdapter {\n return new InstrumentedStorage(storage);\n}\n\nclass InstrumentedStorage implements StorageAdapter {\n private readonly inner: StorageAdapter;\n\n constructor(inner: StorageAdapter) {\n this.inner = inner;\n }\n\n get metadata(): StorageAdapter[\"metadata\"] {\n return this.inner.metadata;\n }\n\n get lifecycle(): StorageAdapter[\"lifecycle\"] {\n return this.inner.lifecycle;\n }\n\n setLogger(logger: Logger): void {\n this.inner.setLogger?.(logger);\n }\n\n async read(path: string): Promise<Buffer> {\n return await track(\"storage.read\", path, async () => await this.inner.read(path));\n }\n\n async write(path: string, data: Buffer): Promise<void> {\n await track(\"storage.write\", path, async () => {\n await this.inner.write(path, data);\n });\n }\n\n async delete(path: string): Promise<void> {\n await track(\"storage.delete\", path, async () => {\n await this.inner.delete(path);\n });\n }\n\n async exists(path: string): Promise<boolean> {\n return await track(\"storage.exists\", path, async () => await this.inner.exists(path));\n }\n\n async list(prefix: string): Promise<string[]> {\n return await track(\"storage.list\", prefix, async () => await this.inner.list(prefix));\n }\n\n async writeStream(path: string, stream: Readable): Promise<void> {\n await track(\"storage.writeStream\", path, async () => {\n await this.inner.writeStream(path, stream);\n });\n }\n\n async readStream(path: string): Promise<Readable> {\n return await track(\"storage.readStream\", path, async () => await this.inner.readStream(path));\n }\n}\n\n/** Run a storage operation inside a span with a duration measurement. */\nasync function track<T>(operation: string, path: string, fn: () => Promise<T>): Promise<T> {\n const start = performance.now();\n try {\n return await withSpan(operation, fn, { \"storage.path\": path });\n } finally {\n const durationMs = performance.now() - start;\n addTiming(\"storage\", durationMs);\n storageMetrics().operationDuration.record(durationMs, {\n \"storage.operation\": operation,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAgB,0BAA0B,SAAyC;CACjF,OAAO,IAAI,oBAAoB,OAAO;AACxC;AAEA,IAAM,sBAAN,MAAoD;CAClD;CAEA,YAAY,OAAuB;EACjC,KAAK,QAAQ;CACf;CAEA,IAAI,WAAuC;EACzC,OAAO,KAAK,MAAM;CACpB;CAEA,IAAI,YAAyC;EAC3C,OAAO,KAAK,MAAM;CACpB;CAEA,UAAU,QAAsB;EAC9B,KAAK,MAAM,YAAY,MAAM;CAC/B;CAEA,MAAM,KAAK,MAA+B;EACxC,OAAO,MAAM,MAAM,gBAAgB,MAAM,YAAY,MAAM,KAAK,MAAM,KAAK,IAAI,CAAC;CAClF;CAEA,MAAM,MAAM,MAAc,MAA6B;EACrD,MAAM,MAAM,iBAAiB,MAAM,YAAY;GAC7C,MAAM,KAAK,MAAM,MAAM,MAAM,IAAI;EACnC,CAAC;CACH;CAEA,MAAM,OAAO,MAA6B;EACxC,MAAM,MAAM,kBAAkB,MAAM,YAAY;GAC9C,MAAM,KAAK,MAAM,OAAO,IAAI;EAC9B,CAAC;CACH;CAEA,MAAM,OAAO,MAAgC;EAC3C,OAAO,MAAM,MAAM,kBAAkB,MAAM,YAAY,MAAM,KAAK,MAAM,OAAO,IAAI,CAAC;CACtF;CAEA,MAAM,KAAK,QAAmC;EAC5C,OAAO,MAAM,MAAM,gBAAgB,QAAQ,YAAY,MAAM,KAAK,MAAM,KAAK,MAAM,CAAC;CACtF;CAEA,MAAM,YAAY,MAAc,QAAiC;EAC/D,MAAM,MAAM,uBAAuB,MAAM,YAAY;GACnD,MAAM,KAAK,MAAM,YAAY,MAAM,MAAM;EAC3C,CAAC;CACH;CAEA,MAAM,WAAW,MAAiC;EAChD,OAAO,MAAM,MAAM,sBAAsB,MAAM,YAAY,MAAM,KAAK,MAAM,WAAW,IAAI,CAAC;CAC9F;AACF;;AAGA,eAAe,MAAS,WAAmB,MAAc,IAAkC;CACzF,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,OAAO,MAAM,SAAS,WAAW,IAAI,EAAE,gBAAgB,KAAK,CAAC;CAC/D,UAAU;EACR,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,UAAU,WAAW,UAAU;EAC/B,eAAe,CAAC,CAAC,kBAAkB,OAAO,YAAY,EACpD,qBAAqB,UACvB,CAAC;CACH;AACF"}
package/dist/logs.d.mts CHANGED
@@ -1,12 +1,10 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/logs.d.ts
4
3
  /**
5
4
  * Build the trace-correlation fields for one log line.
6
5
  *
7
6
  * @returns `{ trace_id, span_id }` when a valid span is active, else `{}`.
8
7
  */
9
8
  declare function otelLogMixin(): Record<string, string>;
10
- //#endregion
11
9
  export { otelLogMixin };
12
10
  //# sourceMappingURL=logs.d.mts.map
package/dist/logs.mjs CHANGED
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { context, trace } from "@opentelemetry/api";
5
- //#region src/logs.ts
5
+ // src/logs.ts
6
6
  /**
7
7
  * Pino log correlation: inject the active trace context into every log line.
8
8
  *
@@ -24,7 +24,6 @@ function otelLogMixin() {
24
24
  span_id: spanContext.spanId
25
25
  };
26
26
  }
27
- //#endregion
28
27
  export { otelLogMixin };
29
28
 
30
29
  //# sourceMappingURL=logs.mjs.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Counter, Histogram } from "@opentelemetry/api";
4
- //#region src/metrics.d.ts
5
4
  /** Capture-pipeline instruments. */
6
5
  interface CaptureInstruments {
7
6
  /** Capture job wall duration in milliseconds (`capture.job.status`). */
@@ -27,6 +26,5 @@ declare function captureMetrics(): CaptureInstruments;
27
26
  declare function dbMetrics(): DbInstruments;
28
27
  /** Storage-operation instruments (created once). */
29
28
  declare function storageMetrics(): StorageInstruments;
30
- //#endregion
31
29
  export { CaptureInstruments, DbInstruments, StorageInstruments, captureMetrics, dbMetrics, storageMetrics };
32
30
  //# sourceMappingURL=metrics.d.mts.map
package/dist/metrics.mjs CHANGED
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { metrics } from "@opentelemetry/api";
5
- //#region src/metrics.ts
5
+ // src/metrics.ts
6
6
  /**
7
7
  * Memoized OTEL instruments (global meter, `storyshelf` scope).
8
8
  *
@@ -38,7 +38,6 @@ function storageMetrics() {
38
38
  if (!storage) storage = { operationDuration: metrics.getMeter(METER_NAME).createHistogram("storage.operation.duration", { unit: "ms" }) };
39
39
  return storage;
40
40
  }
41
- //#endregion
42
41
  export { captureMetrics, dbMetrics, storageMetrics };
43
42
 
44
43
  //# sourceMappingURL=metrics.mjs.map
@@ -2,7 +2,6 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { HttpMiddlewareOptions } from "./types.mjs";
4
4
  import { MiddlewareHandler } from "hono";
5
- //#region src/middleware.d.ts
6
5
  /**
7
6
  * Create the instrumenting middleware.
8
7
  *
@@ -13,6 +12,5 @@ import { MiddlewareHandler } from "hono";
13
12
  * @returns Hono middleware producing one `http.server` span per request.
14
13
  */
15
14
  declare function createHttpMiddleware(options?: HttpMiddlewareOptions): MiddlewareHandler;
16
- //#endregion
17
15
  export { createHttpMiddleware };
18
16
  //# sourceMappingURL=middleware.d.mts.map
@@ -1,9 +1,10 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { redactSecrets } from "@storyshelf/core/utils";
4
5
  import { context, trace } from "@opentelemetry/api";
5
6
  import { httpInstrumentationMiddleware } from "@hono/otel";
6
- //#region src/middleware.ts
7
+ // src/middleware.ts
7
8
  /**
8
9
  * Hono HTTP middleware: `@hono/otel` server spans plus StoryShelf context.
9
10
  *
@@ -16,11 +17,17 @@ import { httpInstrumentationMiddleware } from "@hono/otel";
16
17
  function enrichSpan(c) {
17
18
  const span = trace.getSpan(context.active());
18
19
  if (!span) return;
20
+ redactUrl(c, span);
19
21
  const reqId = c.get("requestId");
20
22
  if (reqId) span.setAttribute("storyshelf.req_id", reqId);
21
23
  const userId = c.get("userId");
22
24
  if (userId) span.setAttribute("enduser.id", userId);
23
25
  }
26
+ /** A signed Storybook URL carries its token in the path (ADR 0007): the span must not keep it. */
27
+ function redactUrl(c, span) {
28
+ const url = redactSecrets(c.req.url);
29
+ if (url !== c.req.url) span.setAttribute("url.full", url);
30
+ }
24
31
  /**
25
32
  * Create the instrumenting middleware.
26
33
  *
@@ -42,7 +49,6 @@ function createHttpMiddleware(options = {}) {
42
49
  });
43
50
  };
44
51
  }
45
- //#endregion
46
52
  export { createHttpMiddleware };
47
53
 
48
54
  //# sourceMappingURL=middleware.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * Hono HTTP middleware: `@hono/otel` server spans plus StoryShelf context.\n *\n * `@hono/otel` owns the `http.server` span (W3C extraction, route-template\n * naming, status attributes, request-duration metrics). This wrapper adds\n * `storyshelf.req_id` and `enduser.id` after downstream handlers run — still\n * inside the span, since `@hono/otel` finalizes after `next()` returns.\n */\nimport { httpInstrumentationMiddleware } from \"@hono/otel\";\nimport { context, trace } from \"@opentelemetry/api\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { HttpMiddlewareOptions } from \"./types.ts\";\n\n/** Copy request/user ids from the Hono context onto the active server span. */\nfunction enrichSpan(c: Context): void {\n const span = trace.getSpan(context.active());\n if (!span) {\n return;\n }\n const reqId = c.get(\"requestId\") as string | undefined;\n if (reqId) {\n span.setAttribute(\"storyshelf.req_id\", reqId);\n }\n const userId = c.get(\"userId\") as string | null | undefined;\n if (userId) {\n span.setAttribute(\"enduser.id\", userId);\n }\n}\n\n/**\n * Create the instrumenting middleware.\n *\n * Mount directly after Hono's `requestId()` so `storyshelf.req_id` resolves;\n * `enduser.id` resolves when `storeScope` runs downstream.\n *\n * @param options - Service name/version for span resources.\n * @returns Hono middleware producing one `http.server` span per request.\n */\nexport function createHttpMiddleware(options: HttpMiddlewareOptions = {}): MiddlewareHandler {\n const otel = httpInstrumentationMiddleware({\n serviceName: options.serviceName ?? \"storyshelf\",\n serviceVersion: options.serviceVersion,\n });\n return async (c, next) => {\n return await otel(c, async () => {\n await next();\n enrichSpan(c);\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,SAAS,WAAW,GAAkB;CACpC,MAAM,OAAO,MAAM,QAAQ,QAAQ,OAAO,CAAC;CAC3C,IAAI,CAAC,MACH;CAEF,MAAM,QAAQ,EAAE,IAAI,WAAW;CAC/B,IAAI,OACF,KAAK,aAAa,qBAAqB,KAAK;CAE9C,MAAM,SAAS,EAAE,IAAI,QAAQ;CAC7B,IAAI,QACF,KAAK,aAAa,cAAc,MAAM;AAE1C;;;;;;;;;;AAWA,SAAgB,qBAAqB,UAAiC,CAAC,GAAsB;CAC3F,MAAM,OAAO,8BAA8B;EACzC,aAAa,QAAQ,eAAe;EACpC,gBAAgB,QAAQ;CAC1B,CAAC;CACD,OAAO,OAAO,GAAG,SAAS;EACxB,OAAO,MAAM,KAAK,GAAG,YAAY;GAC/B,MAAM,KAAK;GACX,WAAW,CAAC;EACd,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"middleware.mjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["/**\n * Hono HTTP middleware: `@hono/otel` server spans plus StoryShelf context.\n *\n * `@hono/otel` owns the `http.server` span (W3C extraction, route-template\n * naming, status attributes, request-duration metrics). This wrapper adds\n * `storyshelf.req_id` and `enduser.id` after downstream handlers run — still\n * inside the span, since `@hono/otel` finalizes after `next()` returns.\n */\nimport { httpInstrumentationMiddleware } from \"@hono/otel\";\nimport { context, trace, type Span } from \"@opentelemetry/api\";\nimport { redactSecrets } from \"@storyshelf/core/utils\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { HttpMiddlewareOptions } from \"./types.ts\";\n\n/** Copy request/user ids from the Hono context onto the active server span. */\nfunction enrichSpan(c: Context): void {\n const span = trace.getSpan(context.active());\n if (!span) {\n return;\n }\n redactUrl(c, span);\n const reqId = c.get(\"requestId\") as string | undefined;\n if (reqId) {\n span.setAttribute(\"storyshelf.req_id\", reqId);\n }\n const userId = c.get(\"userId\") as string | null | undefined;\n if (userId) {\n span.setAttribute(\"enduser.id\", userId);\n }\n}\n\n/** A signed Storybook URL carries its token in the path (ADR 0007): the span must not keep it. */\nfunction redactUrl(c: Context, span: Span): void {\n const url = redactSecrets(c.req.url);\n if (url !== c.req.url) {\n span.setAttribute(\"url.full\", url);\n }\n}\n\n/**\n * Create the instrumenting middleware.\n *\n * Mount directly after Hono's `requestId()` so `storyshelf.req_id` resolves;\n * `enduser.id` resolves when `storeScope` runs downstream.\n *\n * @param options - Service name/version for span resources.\n * @returns Hono middleware producing one `http.server` span per request.\n */\nexport function createHttpMiddleware(options: HttpMiddlewareOptions = {}): MiddlewareHandler {\n const otel = httpInstrumentationMiddleware({\n serviceName: options.serviceName ?? \"storyshelf\",\n serviceVersion: options.serviceVersion,\n });\n return async (c, next) => {\n return await otel(c, async () => {\n await next();\n enrichSpan(c);\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAS,WAAW,GAAkB;CACpC,MAAM,OAAO,MAAM,QAAQ,QAAQ,OAAO,CAAC;CAC3C,IAAI,CAAC,MACH;CAEF,UAAU,GAAG,IAAI;CACjB,MAAM,QAAQ,EAAE,IAAI,WAAW;CAC/B,IAAI,OACF,KAAK,aAAa,qBAAqB,KAAK;CAE9C,MAAM,SAAS,EAAE,IAAI,QAAQ;CAC7B,IAAI,QACF,KAAK,aAAa,cAAc,MAAM;AAE1C;;AAGA,SAAS,UAAU,GAAY,MAAkB;CAC/C,MAAM,MAAM,cAAc,EAAE,IAAI,GAAG;CACnC,IAAI,QAAQ,EAAE,IAAI,KAChB,KAAK,aAAa,YAAY,GAAG;AAErC;;;;;;;;;;AAWA,SAAgB,qBAAqB,UAAiC,CAAC,GAAsB;CAC3F,MAAM,OAAO,8BAA8B;EACzC,aAAa,QAAQ,eAAe;EACpC,gBAAgB,QAAQ;CAC1B,CAAC;CACD,OAAO,OAAO,GAAG,SAAS;EACxB,OAAO,MAAM,KAAK,GAAG,YAAY;GAC/B,MAAM,KAAK;GACX,WAAW,CAAC;EACd,CAAC;CACH;AACF"}
package/dist/node.d.mts CHANGED
@@ -2,7 +2,6 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { ObservabilityHandle } from "./types.mjs";
4
4
  import { ObservabilityConfig, resolveObservabilityConfig } from "./config.mjs";
5
- //#region src/node.d.ts
6
5
  /**
7
6
  * Initialize observability from the environment.
8
7
  *
@@ -15,6 +14,5 @@ import { ObservabilityConfig, resolveObservabilityConfig } from "./config.mjs";
15
14
  * @returns The SDK handle (check `enabled`, always call `shutdown`).
16
15
  */
17
16
  declare function initObservabilityFromEnv(env?: NodeJS.ProcessEnv): Promise<ObservabilityHandle>;
18
- //#endregion
19
17
  export { type ObservabilityConfig, type ObservabilityHandle, initObservabilityFromEnv, resolveObservabilityConfig };
20
18
  //# sourceMappingURL=node.d.mts.map
package/dist/node.mjs CHANGED
@@ -8,7 +8,7 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
8
8
  import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
9
9
  import { NodeSDK } from "@opentelemetry/sdk-node";
10
10
  import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
11
- //#region src/node.ts
11
+ // src/node.ts
12
12
  /**
13
13
  * Node-only OTEL SDK lifecycle.
14
14
  *
@@ -77,7 +77,6 @@ async function initObservabilityFromEnv(env = process.env) {
77
77
  if (!config.enabled) return noopHandle();
78
78
  return startSdk(config);
79
79
  }
80
- //#endregion
81
80
  export { initObservabilityFromEnv, resolveObservabilityConfig };
82
81
 
83
82
  //# sourceMappingURL=node.mjs.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Context } from "@opentelemetry/api";
4
- //#region src/propagate.d.ts
5
4
  /** W3C traceparent header name. */
6
5
  declare const TRACEPARENT_HEADER = "traceparent";
7
6
  /**
@@ -19,6 +18,5 @@ declare function injectTraceContext(headers: Record<string, string>): void;
19
18
  * @returns The header value, or undefined when no span is active.
20
19
  */
21
20
  declare function currentTraceparent(): string | undefined;
22
- //#endregion
23
21
  export { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext };
24
22
  //# sourceMappingURL=propagate.d.mts.map
@@ -2,12 +2,12 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { context, propagation } from "@opentelemetry/api";
5
- //#region src/propagate.ts
5
+ // src/propagate.ts
6
6
  /**
7
7
  * W3C trace-context propagation helpers.
8
8
  *
9
- * Queue payloads carry the `traceparent` string (see `CaptureJob`) so a
10
- * remote worker can continue the enqueueing request's trace; outbound HTTP
9
+ * Queue payloads carry the `traceparent` string (see `CaptureJob`) so the
10
+ * consumer can continue the enqueueing request's trace; outbound HTTP
11
11
  * injects the same header via {@link injectTraceContext}.
12
12
  */
13
13
  /** W3C traceparent header name. */
@@ -36,7 +36,6 @@ function currentTraceparent() {
36
36
  propagation.inject(context.active(), carrier);
37
37
  return carrier[TRACEPARENT_HEADER];
38
38
  }
39
- //#endregion
40
39
  export { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext };
41
40
 
42
41
  //# sourceMappingURL=propagate.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"propagate.mjs","names":[],"sources":["../src/propagate.ts"],"sourcesContent":["/**\n * W3C trace-context propagation helpers.\n *\n * Queue payloads carry the `traceparent` string (see `CaptureJob`) so a\n * remote worker can continue the enqueueing request's trace; outbound HTTP\n * injects the same header via {@link injectTraceContext}.\n */\nimport { context, propagation, type Context } from \"@opentelemetry/api\";\n\n/** W3C traceparent header name. */\nexport const TRACEPARENT_HEADER = \"traceparent\";\n\n/**\n * Resolve the parent context from an inbound `traceparent` value.\n *\n * @param traceparent - W3C value from a queue payload; undefined keeps context.\n * @returns The extracted remote context, or the active context when absent.\n */\nexport function parentContext(traceparent?: string): Context {\n if (!traceparent) {\n return context.active();\n }\n return propagation.extract(context.active(), { [TRACEPARENT_HEADER]: traceparent });\n}\n\n/** Inject the active span context into a mutable header record. */\nexport function injectTraceContext(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\n/**\n * Read the current W3C `traceparent` value for queue payloads.\n *\n * @returns The header value, or undefined when no span is active.\n */\nexport function currentTraceparent(): string | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n return carrier[TRACEPARENT_HEADER];\n}\n"],"mappings":";;;;;;;;;;;;;AAUA,MAAa,qBAAqB;;;;;;;AAQlC,SAAgB,cAAc,aAA+B;CAC3D,IAAI,CAAC,aACH,OAAO,QAAQ,OAAO;CAExB,OAAO,YAAY,QAAQ,QAAQ,OAAO,GAAG,GAAG,qBAAqB,YAAY,CAAC;AACpF;;AAGA,SAAgB,mBAAmB,SAAuC;CACxE,YAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAC9C;;;;;;AAOA,SAAgB,qBAAyC;CACvD,MAAM,UAAkC,CAAC;CACzC,YAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;CAC5C,OAAO,QAAQ;AACjB"}
1
+ {"version":3,"file":"propagate.mjs","names":[],"sources":["../src/propagate.ts"],"sourcesContent":["/**\n * W3C trace-context propagation helpers.\n *\n * Queue payloads carry the `traceparent` string (see `CaptureJob`) so the\n * consumer can continue the enqueueing request's trace; outbound HTTP\n * injects the same header via {@link injectTraceContext}.\n */\nimport { context, propagation, type Context } from \"@opentelemetry/api\";\n\n/** W3C traceparent header name. */\nexport const TRACEPARENT_HEADER = \"traceparent\";\n\n/**\n * Resolve the parent context from an inbound `traceparent` value.\n *\n * @param traceparent - W3C value from a queue payload; undefined keeps context.\n * @returns The extracted remote context, or the active context when absent.\n */\nexport function parentContext(traceparent?: string): Context {\n if (!traceparent) {\n return context.active();\n }\n return propagation.extract(context.active(), { [TRACEPARENT_HEADER]: traceparent });\n}\n\n/** Inject the active span context into a mutable header record. */\nexport function injectTraceContext(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\n/**\n * Read the current W3C `traceparent` value for queue payloads.\n *\n * @returns The header value, or undefined when no span is active.\n */\nexport function currentTraceparent(): string | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n return carrier[TRACEPARENT_HEADER];\n}\n"],"mappings":";;;;;;;;;;;;;AAUA,MAAa,qBAAqB;;;;;;;AAQlC,SAAgB,cAAc,aAA+B;CAC3D,IAAI,CAAC,aACH,OAAO,QAAQ,OAAO;CAExB,OAAO,YAAY,QAAQ,QAAQ,OAAO,GAAG,GAAG,qBAAqB,YAAY,CAAC;AACpF;;AAGA,SAAgB,mBAAmB,SAAuC;CACxE,YAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAC9C;;;;;;AAOA,SAAgB,qBAAyC;CACvD,MAAM,UAAkC,CAAC;CACzC,YAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;CAC5C,OAAO,QAAQ;AACjB"}
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Attributes, Context, Span } from "@opentelemetry/api";
4
- //#region src/tracing.d.ts
5
4
  /**
6
5
  * Run `fn` inside an internal span.
7
6
  *
@@ -15,6 +14,5 @@ import { Attributes, Context, Span } from "@opentelemetry/api";
15
14
  * @returns Whatever `fn` returns.
16
15
  */
17
16
  declare function withSpan<T>(name: string, fn: (span: Span) => Promise<T>, attributes?: Attributes, parent?: Context): Promise<T>;
18
- //#endregion
19
17
  export { withSpan };
20
18
  //# sourceMappingURL=tracing.d.mts.map
package/dist/tracing.mjs CHANGED
@@ -1,13 +1,30 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { HttpError, redactSecrets } from "@storyshelf/core/utils";
4
5
  import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
5
- //#region src/tracing.ts
6
+ // src/tracing.ts
6
7
  /**
7
8
  * Span helpers over `@opentelemetry/api` (runtime-agnostic: Node SDK or
8
9
  * Deno's native bridge). Without a configured provider these are cheap
9
10
  * noops, so call sites need no enabled-checks.
10
11
  */
12
+ /**
13
+ * What a span records of a caught error: a copy with the same `name` and a `redactSecrets` message
14
+ * (connection strings, tokens and keys stay out of exported spans), and for an `HttpError` its
15
+ * status only, never the response body. The caller still gets the original error rethrown.
16
+ * Intentional copy of core's `spanException` (ADR 0015 §5 no-cycle rule: do not deduplicate).
17
+ */
18
+ function recordedError(error) {
19
+ if (!(error instanceof Error)) return redactSecrets(String(error));
20
+ const message = error instanceof HttpError ? `HTTP ${String(error.status)}` : redactSecrets(error.message);
21
+ const copy = new Error(message);
22
+ copy.name = error.name;
23
+ const header = `${error.name}: ${error.message}`;
24
+ const frames = error.stack?.startsWith(header) ? error.stack.slice(header.length) : "";
25
+ copy.stack = `${error.name}: ${message}${redactSecrets(frames)}`;
26
+ return copy;
27
+ }
11
28
  /** Instrumentation scope for observability-package spans. */
12
29
  const SCOPE = "storyshelf.observability";
13
30
  /**
@@ -30,7 +47,7 @@ async function withSpan(name, fn, attributes, parent) {
30
47
  try {
31
48
  return await fn(span);
32
49
  } catch (error) {
33
- span.recordException(error instanceof Error ? error : String(error));
50
+ span.recordException(recordedError(error));
34
51
  span.setStatus({ code: SpanStatusCode.ERROR });
35
52
  throw error;
36
53
  } finally {
@@ -38,7 +55,6 @@ async function withSpan(name, fn, attributes, parent) {
38
55
  }
39
56
  });
40
57
  }
41
- //#endregion
42
58
  export { withSpan };
43
59
 
44
60
  //# sourceMappingURL=tracing.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracing.mjs","names":[],"sources":["../src/tracing.ts"],"sourcesContent":["/**\n * Span helpers over `@opentelemetry/api` (runtime-agnostic: Node SDK or\n * Deno's native bridge). Without a configured provider these are cheap\n * noops, so call sites need no enabled-checks.\n */\nimport {\n SpanKind,\n SpanStatusCode,\n context,\n trace,\n type Attributes,\n type Context,\n type Span,\n} from \"@opentelemetry/api\";\n\n/** Instrumentation scope for observability-package spans. */\nconst SCOPE = \"storyshelf.observability\";\n\n/**\n * Run `fn` inside an internal span.\n *\n * On throw the span records the exception, is marked ERROR, and the error\n * is rethrown — mirroring the pino `{ err }` convention.\n *\n * @param name - Span name (`domain.operation`, never a high-cardinality value).\n * @param fn - Work to run with the span active.\n * @param attributes - Fixed span attributes (no high-cardinality values in names).\n * @param parent - Parent context (defaults to the active context).\n * @returns Whatever `fn` returns.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T>,\n attributes?: Attributes,\n parent?: Context,\n): Promise<T> {\n const tracer = trace.getTracer(SCOPE);\n return await tracer.startActiveSpan(\n name,\n { kind: SpanKind.INTERNAL, attributes },\n parent ?? context.active(),\n async (span) => {\n try {\n return await fn(span);\n } catch (error) {\n span.recordException(error instanceof Error ? error : String(error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n );\n}\n"],"mappings":";;;;;;;;;;;AAgBA,MAAM,QAAQ;;;;;;;;;;;;;AAcd,eAAsB,SACpB,MACA,IACA,YACA,QACY;CAEZ,OAAO,MADQ,MAAM,UAAU,KACb,CAAC,CAAC,gBAClB,MACA;EAAE,MAAM,SAAS;EAAU;CAAW,GACtC,UAAU,QAAQ,OAAO,GACzB,OAAO,SAAS;EACd,IAAI;GACF,OAAO,MAAM,GAAG,IAAI;EACtB,SAAS,OAAO;GACd,KAAK,gBAAgB,iBAAiB,QAAQ,QAAQ,OAAO,KAAK,CAAC;GACnE,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;GAC7C,MAAM;EACR,UAAU;GACR,KAAK,IAAI;EACX;CACF,CACF;AACF"}
1
+ {"version":3,"file":"tracing.mjs","names":[],"sources":["../src/tracing.ts"],"sourcesContent":["/**\n * Span helpers over `@opentelemetry/api` (runtime-agnostic: Node SDK or\n * Deno's native bridge). Without a configured provider these are cheap\n * noops, so call sites need no enabled-checks.\n */\nimport {\n SpanKind,\n SpanStatusCode,\n context,\n trace,\n type Attributes,\n type Context,\n type Span,\n} from \"@opentelemetry/api\";\nimport { HttpError, redactSecrets } from \"@storyshelf/core/utils\";\n\n/**\n * What a span records of a caught error: a copy with the same `name` and a `redactSecrets` message\n * (connection strings, tokens and keys stay out of exported spans), and for an `HttpError` its\n * status only, never the response body. The caller still gets the original error rethrown.\n * Intentional copy of core's `spanException` (ADR 0015 §5 no-cycle rule: do not deduplicate).\n */\nfunction recordedError(error: unknown): Error | string {\n if (!(error instanceof Error)) {\n return redactSecrets(String(error));\n }\n const message =\n error instanceof HttpError ? `HTTP ${String(error.status)}` : redactSecrets(error.message);\n const copy = new Error(message);\n copy.name = error.name;\n const header = `${error.name}: ${error.message}`;\n const frames = error.stack?.startsWith(header) ? error.stack.slice(header.length) : \"\";\n copy.stack = `${error.name}: ${message}${redactSecrets(frames)}`;\n return copy;\n}\n\n/** Instrumentation scope for observability-package spans. */\nconst SCOPE = \"storyshelf.observability\";\n\n/**\n * Run `fn` inside an internal span.\n *\n * On throw the span records the exception, is marked ERROR, and the error\n * is rethrown — mirroring the pino `{ err }` convention.\n *\n * @param name - Span name (`domain.operation`, never a high-cardinality value).\n * @param fn - Work to run with the span active.\n * @param attributes - Fixed span attributes (no high-cardinality values in names).\n * @param parent - Parent context (defaults to the active context).\n * @returns Whatever `fn` returns.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T>,\n attributes?: Attributes,\n parent?: Context,\n): Promise<T> {\n const tracer = trace.getTracer(SCOPE);\n return await tracer.startActiveSpan(\n name,\n { kind: SpanKind.INTERNAL, attributes },\n parent ?? context.active(),\n async (span) => {\n try {\n return await fn(span);\n } catch (error) {\n span.recordException(recordedError(error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,SAAS,cAAc,OAAgC;CACrD,IAAI,EAAE,iBAAiB,QACrB,OAAO,cAAc,OAAO,KAAK,CAAC;CAEpC,MAAM,UACJ,iBAAiB,YAAY,QAAQ,OAAO,MAAM,MAAM,MAAM,cAAc,MAAM,OAAO;CAC3F,MAAM,OAAO,IAAI,MAAM,OAAO;CAC9B,KAAK,OAAO,MAAM;CAClB,MAAM,SAAS,GAAG,MAAM,KAAK,IAAI,MAAM;CACvC,MAAM,SAAS,MAAM,OAAO,WAAW,MAAM,IAAI,MAAM,MAAM,MAAM,OAAO,MAAM,IAAI;CACpF,KAAK,QAAQ,GAAG,MAAM,KAAK,IAAI,UAAU,cAAc,MAAM;CAC7D,OAAO;AACT;;AAGA,MAAM,QAAQ;;;;;;;;;;;;;AAcd,eAAsB,SACpB,MACA,IACA,YACA,QACY;CAEZ,OAAO,MADQ,MAAM,UAAU,KACb,CAAC,CAAC,gBAClB,MACA;EAAE,MAAM,SAAS;EAAU;CAAW,GACtC,UAAU,QAAQ,OAAO,GACzB,OAAO,SAAS;EACd,IAAI;GACF,OAAO,MAAM,GAAG,IAAI;EACtB,SAAS,OAAO;GACd,KAAK,gBAAgB,cAAc,KAAK,CAAC;GACzC,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;GAC7C,MAAM;EACR,UAAU;GACR,KAAK,IAAI;EACX;CACF,CACF;AACF"}
package/dist/types.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Attributes } from "@opentelemetry/api";
4
- //#region src/types.d.ts
5
4
  /** Attributes attached to a span or metric measurement. */
6
5
  type SpanAttributes = Attributes;
7
6
  /** Shutdown handle for the observability SDK (see `node.ts`). */
@@ -18,6 +17,5 @@ interface HttpMiddlewareOptions {
18
17
  /** Service version in spans/metrics. Defaults to the package version. */
19
18
  serviceVersion?: string;
20
19
  }
21
- //#endregion
22
20
  export { HttpMiddlewareOptions, ObservabilityHandle, SpanAttributes };
23
21
  //# sourceMappingURL=types.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/observability",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "StoryShelf observability: OpenTelemetry tracing, metrics, and log correlation over @storyshelf/core adapters.",
5
5
  "keywords": [
6
6
  "metrics",
@@ -29,9 +29,6 @@
29
29
  "dist"
30
30
  ],
31
31
  "type": "module",
32
- "engines": {
33
- "node": ">=24"
34
- },
35
32
  "sideEffects": false,
36
33
  "types": "./dist/index.d.mts",
37
34
  "exports": {
@@ -68,8 +65,8 @@
68
65
  "@opentelemetry/sdk-metrics": "^2.11.0",
69
66
  "@opentelemetry/sdk-node": "^0.222.0",
70
67
  "@opentelemetry/semantic-conventions": "^1.43.0",
71
- "@storyshelf/core": "0.6.2",
72
- "drizzle-orm": "^0.44.0",
68
+ "@storyshelf/core": "0.8.0",
69
+ "drizzle-orm": "^0.45.4",
73
70
  "hono": "^4.11.0"
74
71
  },
75
72
  "devDependencies": {
@@ -85,7 +82,10 @@
85
82
  "oxlint-tsgolint": "^7.0.2001",
86
83
  "tsdown": "^0.22.14",
87
84
  "typescript": "^7.0.2",
88
- "vitest": "^5.0.0"
85
+ "vitest": "^5.0.5"
86
+ },
87
+ "engines": {
88
+ "node": ">=24"
89
89
  },
90
90
  "jsr": {
91
91
  "runtimeCompat": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * StoryShelf observability: runtime-agnostic OTEL instrumentation.\n *\n * This entrypoint is cross-runtime (`@opentelemetry/api` only — Node SDK or\n * Deno's native bridge supplies the provider). The Node SDK lifecycle\n * (`initObservabilityFromEnv`) lives under `@storyshelf/observability/node`\n * and must never be imported here.\n */\nimport { createInstrumentedDatabase } from \"./instrument-db.ts\";\nimport { createInstrumentedStorage } from \"./instrument-storage.ts\";\nimport { createHttpMiddleware } from \"./middleware.ts\";\n\n/** Wrap a database adapter with `db.*` spans and metrics. */\nexport { createInstrumentedDatabase };\n/** Wrap a storage adapter with `storage.*` spans and metrics. */\nexport { createInstrumentedStorage };\n/** Hono middleware emitting one `http.server` span per request. */\nexport { createHttpMiddleware };\nexport { captureMetrics, dbMetrics, storageMetrics } from \"./metrics.ts\";\nexport type { CaptureInstruments, DbInstruments, StorageInstruments } from \"./metrics.ts\";\nexport { otelLogMixin } from \"./logs.ts\";\nexport {\n TRACEPARENT_HEADER,\n currentTraceparent,\n injectTraceContext,\n parentContext,\n} from \"./propagate.ts\";\nexport { resolveObservabilityConfig } from \"./config.ts\";\nexport type { ObservabilityConfig } from \"./config.ts\";\nexport { withSpan } from \"./tracing.ts\";\nexport type { HttpMiddlewareOptions, ObservabilityHandle, SpanAttributes } from \"./types.ts\";\n"],"mappings":""}