@storyshelf/observability 0.0.0-stage → 0.5.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Siddhant Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # @storyshelf/observability
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ OpenTelemetry tracing, metrics, and log correlation for StoryShelf adapters. This is the single OTEL owner in the monorepo.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/observability
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/observability
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { createInstrumentedDatabase, createInstrumentedStorage } from "@storyshelf/observability";
21
+
22
+ const database = createInstrumentedDatabase(baseDatabase);
23
+ const storage = createInstrumentedStorage(baseStorage);
24
+ ```
25
+
26
+ See the [StoryShelf documentation](https://github.com/GuptaSiddhant/StoryShelf#readme) for configuration details.
27
+
28
+ ## License
29
+
30
+ MIT
@@ -0,0 +1,38 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/config.d.ts
4
+ /**
5
+ * Environment-driven observability configuration.
6
+ *
7
+ * Standard `OTEL_*` variables win; `STORYSHELF_OTEL_*` covers StoryShelf
8
+ * specifics. When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset the SDK stays a
9
+ * noop (zero overhead, hermetic tests). Deno runtimes are always noop here —
10
+ * Deno's built-in OTEL (`OTEL_DENO=true`) bridges `npm:@opentelemetry/api`
11
+ * natively, so a NodeSDK must never start under Deno.
12
+ */
13
+ /** Resolved observability configuration for the Node SDK entrypoint. */
14
+ interface ObservabilityConfig {
15
+ /** Whether to start the SDK (false = noop handle). */
16
+ enabled: boolean;
17
+ /** OTLP/HTTP endpoint, e.g. `http://collector:4318`. */
18
+ endpoint?: string;
19
+ /** Extra OTLP headers (`OTEL_EXPORTER_OTLP_HEADERS`, `k=v,k2=v2`). */
20
+ headers?: Record<string, string>;
21
+ /** Resource `service.name`. */
22
+ serviceName: string;
23
+ /** Resource `service.version`. */
24
+ serviceVersion?: string;
25
+ /** Metric export interval in milliseconds. */
26
+ metricsIntervalMs: number;
27
+ }
28
+ /**
29
+ * Resolve the observability configuration from the environment.
30
+ *
31
+ * @param env - Environment record (defaults to `process.env`).
32
+ * @param serviceVersion - Version for the resource (defaults to `__PKG_VERSION__`).
33
+ * @returns The resolved config; `enabled` is false without an endpoint.
34
+ */
35
+ declare function resolveObservabilityConfig(env?: NodeJS.ProcessEnv, serviceVersion?: string): ObservabilityConfig;
36
+ //#endregion
37
+ export { ObservabilityConfig, resolveObservabilityConfig };
38
+ //# sourceMappingURL=config.d.mts.map
@@ -0,0 +1,60 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/config.ts
5
+ const DEFAULT_SERVICE_NAME = "storyshelf";
6
+ const DEFAULT_METRICS_INTERVAL_MS = 6e4;
7
+ /** Whether the current runtime is Deno (native OTEL owns the SDK there). */
8
+ function isDenoRuntime(env) {
9
+ if (env["OTEL_DENO"] === "true") return true;
10
+ return globalThis.Deno !== void 0;
11
+ }
12
+ /** Parse one `k=v` pair into the headers record. */
13
+ function parseHeaderPair(pair, headers) {
14
+ const separator = pair.indexOf("=");
15
+ if (separator === -1) return;
16
+ const key = pair.slice(0, separator).trim();
17
+ const value = pair.slice(separator + 1).trim();
18
+ if (key) headers[key] = value;
19
+ }
20
+ /** Parse `k=v,k2=v2` header syntax into a record. */
21
+ function parseHeaders(raw) {
22
+ if (!raw) return;
23
+ const headers = {};
24
+ for (const pair of raw.split(",")) parseHeaderPair(pair, headers);
25
+ return Object.keys(headers).length === 0 ? void 0 : headers;
26
+ }
27
+ /** Parse a positive integer env value with a fallback. */
28
+ function parseInterval(raw, fallback) {
29
+ const parsed = Number(raw);
30
+ if (!raw || !Number.isFinite(parsed) || parsed <= 0) return fallback;
31
+ return Math.trunc(parsed);
32
+ }
33
+ /**
34
+ * Resolve the observability configuration from the environment.
35
+ *
36
+ * @param env - Environment record (defaults to `process.env`).
37
+ * @param serviceVersion - Version for the resource (defaults to `__PKG_VERSION__`).
38
+ * @returns The resolved config; `enabled` is false without an endpoint.
39
+ */
40
+ function resolveObservabilityConfig(env = process.env, serviceVersion) {
41
+ const endpoint = env["OTEL_EXPORTER_OTLP_ENDPOINT"];
42
+ const sdkDisabled = env["OTEL_SDK_DISABLED"] === "true";
43
+ const explicit = env["STORYSHELF_OTEL_ENABLED"];
44
+ return {
45
+ enabled: !sdkDisabled && !isDenoRuntime(env) && (explicit ? explicit === "true" : Boolean(endpoint)),
46
+ endpoint,
47
+ headers: parseHeaders(env["OTEL_EXPORTER_OTLP_HEADERS"]),
48
+ serviceName: env["OTEL_SERVICE_NAME"] ?? DEFAULT_SERVICE_NAME,
49
+ serviceVersion: serviceVersion ?? packageVersion(),
50
+ metricsIntervalMs: parseInterval(env["STORYSHELF_OTEL_METRICS_INTERVAL_MS"], DEFAULT_METRICS_INTERVAL_MS)
51
+ };
52
+ }
53
+ /** Package version injected at build via __PKG_VERSION__. */
54
+ function packageVersion() {
55
+ return globalThis.__PKG_VERSION__;
56
+ }
57
+ //#endregion
58
+ export { resolveObservabilityConfig };
59
+
60
+ //# sourceMappingURL=config.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,12 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { createInstrumentedDatabase } from "./instrument-db.mjs";
4
+ import { createInstrumentedStorage } from "./instrument-storage.mjs";
5
+ import { HttpMiddlewareOptions, ObservabilityHandle, SpanAttributes } from "./types.mjs";
6
+ import { createHttpMiddleware } from "./middleware.mjs";
7
+ import { CaptureInstruments, DbInstruments, StorageInstruments, captureMetrics, dbMetrics, storageMetrics } from "./metrics.mjs";
8
+ import { otelLogMixin } from "./logs.mjs";
9
+ import { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext } from "./propagate.mjs";
10
+ import { ObservabilityConfig, resolveObservabilityConfig } from "./config.mjs";
11
+ import { withSpan } from "./tracing.mjs";
12
+ export { type CaptureInstruments, type DbInstruments, type HttpMiddlewareOptions, type ObservabilityConfig, type ObservabilityHandle, type SpanAttributes, type StorageInstruments, TRACEPARENT_HEADER, captureMetrics, createHttpMiddleware, createInstrumentedDatabase, createInstrumentedStorage, currentTraceparent, dbMetrics, injectTraceContext, otelLogMixin, parentContext, resolveObservabilityConfig, storageMetrics, withSpan };
package/dist/index.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { captureMetrics, dbMetrics, storageMetrics } from "./metrics.mjs";
5
+ import { withSpan } from "./tracing.mjs";
6
+ import { createInstrumentedDatabase } from "./instrument-db.mjs";
7
+ import { createInstrumentedStorage } from "./instrument-storage.mjs";
8
+ import { createHttpMiddleware } from "./middleware.mjs";
9
+ import { otelLogMixin } from "./logs.mjs";
10
+ import { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext } from "./propagate.mjs";
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
+ export { TRACEPARENT_HEADER, captureMetrics, createHttpMiddleware, createInstrumentedDatabase, createInstrumentedStorage, currentTraceparent, dbMetrics, injectTraceContext, otelLogMixin, parentContext, resolveObservabilityConfig, storageMetrics, withSpan };
23
+
24
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
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":""}
@@ -0,0 +1,14 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
4
+ //#region src/instrument-db.d.ts
5
+ /**
6
+ * Wrap a database adapter with per-operation spans and metrics.
7
+ *
8
+ * @param db - The adapter to wrap (never mutated).
9
+ * @returns A transparent adapter emitting `db.*` telemetry.
10
+ */
11
+ declare function createInstrumentedDatabase(db: DatabaseAdapter): DatabaseAdapter;
12
+ //#endregion
13
+ export { createInstrumentedDatabase };
14
+ //# sourceMappingURL=instrument-db.d.mts.map
@@ -0,0 +1,88 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { dbMetrics } from "./metrics.mjs";
5
+ import { withSpan } from "./tracing.mjs";
6
+ import { addTiming } from "@storyshelf/core/utils";
7
+ import { getTableName } from "drizzle-orm";
8
+ //#region src/instrument-db.ts
9
+ /**
10
+ * Wrap a database adapter with per-operation spans and metrics.
11
+ *
12
+ * @param db - The adapter to wrap (never mutated).
13
+ * @returns A transparent adapter emitting `db.*` telemetry.
14
+ */
15
+ function createInstrumentedDatabase(db) {
16
+ return new InstrumentedDatabase(db);
17
+ }
18
+ var InstrumentedDatabase = class {
19
+ inner;
20
+ constructor(inner) {
21
+ this.inner = inner;
22
+ }
23
+ get metadata() {
24
+ return this.inner.metadata;
25
+ }
26
+ get lifecycle() {
27
+ return this.inner.lifecycle;
28
+ }
29
+ get tables() {
30
+ return this.inner.tables;
31
+ }
32
+ setLogger(logger) {
33
+ this.inner.setLogger?.(logger);
34
+ }
35
+ async insert(table, values) {
36
+ return await track("db.insert", table, async () => await this.inner.insert(table, values));
37
+ }
38
+ async update(table, id, values) {
39
+ return await track("db.update", table, async () => await this.inner.update(table, id, values));
40
+ }
41
+ async get(table, id) {
42
+ return await track("db.get", table, async () => await this.inner.get(table, id));
43
+ }
44
+ async remove(table, id) {
45
+ await track("db.remove", table, async () => {
46
+ await this.inner.remove(table, id);
47
+ });
48
+ }
49
+ async list(table, opts) {
50
+ return await track("db.list", table, async () => await this.inner.list(table, opts));
51
+ }
52
+ async count(table, where) {
53
+ return await track("db.count", table, async () => await this.inner.count(table, where));
54
+ }
55
+ async all(query) {
56
+ return await trackQuery("db.all", async () => await this.inner.all(query));
57
+ }
58
+ };
59
+ /** Run a table operation inside a span with a duration measurement. */
60
+ async function track(operation, table, fn) {
61
+ const tableName = getTableName(table);
62
+ const start = performance.now();
63
+ try {
64
+ return await withSpan(operation, fn, { "db.table": tableName });
65
+ } finally {
66
+ const durationMs = performance.now() - start;
67
+ addTiming("db", durationMs);
68
+ dbMetrics().operationDuration.record(durationMs, {
69
+ "db.operation": operation,
70
+ "db.table": tableName
71
+ });
72
+ }
73
+ }
74
+ /** Run a table-less query inside a span with a duration measurement. */
75
+ async function trackQuery(operation, fn) {
76
+ const start = performance.now();
77
+ try {
78
+ return await withSpan(operation, fn);
79
+ } finally {
80
+ const durationMs = performance.now() - start;
81
+ addTiming("db", durationMs);
82
+ dbMetrics().operationDuration.record(durationMs, { "db.operation": operation });
83
+ }
84
+ }
85
+ //#endregion
86
+ export { createInstrumentedDatabase };
87
+
88
+ //# sourceMappingURL=instrument-db.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,14 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { StorageAdapter } from "@storyshelf/core/adapter/storage";
4
+ //#region src/instrument-storage.d.ts
5
+ /**
6
+ * Wrap a storage adapter with per-operation spans and metrics.
7
+ *
8
+ * @param storage - The adapter to wrap (never mutated).
9
+ * @returns A transparent adapter emitting `storage.*` telemetry.
10
+ */
11
+ declare function createInstrumentedStorage(storage: StorageAdapter): StorageAdapter;
12
+ //#endregion
13
+ export { createInstrumentedStorage };
14
+ //# sourceMappingURL=instrument-storage.d.mts.map
@@ -0,0 +1,73 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { storageMetrics } from "./metrics.mjs";
5
+ import { withSpan } from "./tracing.mjs";
6
+ import { addTiming } from "@storyshelf/core/utils";
7
+ //#region src/instrument-storage.ts
8
+ /**
9
+ * Wrap a storage adapter with per-operation spans and metrics.
10
+ *
11
+ * @param storage - The adapter to wrap (never mutated).
12
+ * @returns A transparent adapter emitting `storage.*` telemetry.
13
+ */
14
+ function createInstrumentedStorage(storage) {
15
+ return new InstrumentedStorage(storage);
16
+ }
17
+ var InstrumentedStorage = class {
18
+ inner;
19
+ constructor(inner) {
20
+ this.inner = inner;
21
+ }
22
+ get metadata() {
23
+ return this.inner.metadata;
24
+ }
25
+ get lifecycle() {
26
+ return this.inner.lifecycle;
27
+ }
28
+ setLogger(logger) {
29
+ this.inner.setLogger?.(logger);
30
+ }
31
+ async read(path) {
32
+ return await track("storage.read", path, async () => await this.inner.read(path));
33
+ }
34
+ async write(path, data) {
35
+ await track("storage.write", path, async () => {
36
+ await this.inner.write(path, data);
37
+ });
38
+ }
39
+ async delete(path) {
40
+ await track("storage.delete", path, async () => {
41
+ await this.inner.delete(path);
42
+ });
43
+ }
44
+ async exists(path) {
45
+ return await track("storage.exists", path, async () => await this.inner.exists(path));
46
+ }
47
+ async list(prefix) {
48
+ return await track("storage.list", prefix, async () => await this.inner.list(prefix));
49
+ }
50
+ async writeStream(path, stream) {
51
+ await track("storage.writeStream", path, async () => {
52
+ await this.inner.writeStream(path, stream);
53
+ });
54
+ }
55
+ async readStream(path) {
56
+ return await track("storage.readStream", path, async () => await this.inner.readStream(path));
57
+ }
58
+ };
59
+ /** Run a storage operation inside a span with a duration measurement. */
60
+ async function track(operation, path, fn) {
61
+ const start = performance.now();
62
+ try {
63
+ return await withSpan(operation, fn, { "storage.path": path });
64
+ } finally {
65
+ const durationMs = performance.now() - start;
66
+ addTiming("storage", durationMs);
67
+ storageMetrics().operationDuration.record(durationMs, { "storage.operation": operation });
68
+ }
69
+ }
70
+ //#endregion
71
+ export { createInstrumentedStorage };
72
+
73
+ //# sourceMappingURL=instrument-storage.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,12 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/logs.d.ts
4
+ /**
5
+ * Build the trace-correlation fields for one log line.
6
+ *
7
+ * @returns `{ trace_id, span_id }` when a valid span is active, else `{}`.
8
+ */
9
+ declare function otelLogMixin(): Record<string, string>;
10
+ //#endregion
11
+ export { otelLogMixin };
12
+ //# sourceMappingURL=logs.d.mts.map
package/dist/logs.mjs ADDED
@@ -0,0 +1,30 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { context, trace } from "@opentelemetry/api";
5
+ //#region src/logs.ts
6
+ /**
7
+ * Pino log correlation: inject the active trace context into every log line.
8
+ *
9
+ * Pass `otelLogMixin` as the pino `mixin` (see `LoggerOptions.mixin`) so
10
+ * stdout JSON carries `trace_id`/`span_id` whenever a span is active. The
11
+ * collector joins these with exported traces — this is the log signal
12
+ * (no separate OTLP log exporter; pino stays the single log pipeline).
13
+ */
14
+ /**
15
+ * Build the trace-correlation fields for one log line.
16
+ *
17
+ * @returns `{ trace_id, span_id }` when a valid span is active, else `{}`.
18
+ */
19
+ function otelLogMixin() {
20
+ const spanContext = trace.getSpan(context.active())?.spanContext();
21
+ if (!spanContext || spanContext.traceId === "00000000000000000000000000000000") return {};
22
+ return {
23
+ trace_id: spanContext.traceId,
24
+ span_id: spanContext.spanId
25
+ };
26
+ }
27
+ //#endregion
28
+ export { otelLogMixin };
29
+
30
+ //# sourceMappingURL=logs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.mjs","names":[],"sources":["../src/logs.ts"],"sourcesContent":["/**\n * Pino log correlation: inject the active trace context into every log line.\n *\n * Pass `otelLogMixin` as the pino `mixin` (see `LoggerOptions.mixin`) so\n * stdout JSON carries `trace_id`/`span_id` whenever a span is active. The\n * collector joins these with exported traces — this is the log signal\n * (no separate OTLP log exporter; pino stays the single log pipeline).\n */\nimport { context, trace } from \"@opentelemetry/api\";\n\n/**\n * Build the trace-correlation fields for one log line.\n *\n * @returns `{ trace_id, span_id }` when a valid span is active, else `{}`.\n */\nexport function otelLogMixin(): Record<string, string> {\n const spanContext = trace.getSpan(context.active())?.spanContext();\n if (!spanContext || spanContext.traceId === \"00000000000000000000000000000000\") {\n return {};\n }\n return { trace_id: spanContext.traceId, span_id: spanContext.spanId };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAeA,SAAgB,eAAuC;CACrD,MAAM,cAAc,MAAM,QAAQ,QAAQ,OAAO,CAAC,CAAC,EAAE,YAAY;CACjE,IAAI,CAAC,eAAe,YAAY,YAAY,oCAC1C,OAAO,CAAC;CAEV,OAAO;EAAE,UAAU,YAAY;EAAS,SAAS,YAAY;CAAO;AACtE"}
@@ -0,0 +1,32 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Counter, Histogram } from "@opentelemetry/api";
4
+ //#region src/metrics.d.ts
5
+ /** Capture-pipeline instruments. */
6
+ interface CaptureInstruments {
7
+ /** Capture job wall duration in milliseconds (`capture.job.status`). */
8
+ jobDuration: Histogram;
9
+ /** Completed jobs (`capture.job.status = completed`). */
10
+ jobsCompleted: Counter;
11
+ /** Failed jobs (`capture.job.status = failed`). */
12
+ jobsFailed: Counter;
13
+ }
14
+ /** Database-operation instruments. */
15
+ interface DbInstruments {
16
+ /** Per-operation wall duration in milliseconds (`db.operation`, `db.table`). */
17
+ operationDuration: Histogram;
18
+ }
19
+ /** Storage-operation instruments. */
20
+ interface StorageInstruments {
21
+ /** Per-operation wall duration in milliseconds (`storage.operation`). */
22
+ operationDuration: Histogram;
23
+ }
24
+ /** Capture-pipeline instruments (created once). */
25
+ declare function captureMetrics(): CaptureInstruments;
26
+ /** Database-operation instruments (created once). */
27
+ declare function dbMetrics(): DbInstruments;
28
+ /** Storage-operation instruments (created once). */
29
+ declare function storageMetrics(): StorageInstruments;
30
+ //#endregion
31
+ export { CaptureInstruments, DbInstruments, StorageInstruments, captureMetrics, dbMetrics, storageMetrics };
32
+ //# sourceMappingURL=metrics.d.mts.map
@@ -0,0 +1,44 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { metrics } from "@opentelemetry/api";
5
+ //#region src/metrics.ts
6
+ /**
7
+ * Memoized OTEL instruments (global meter, `storyshelf` scope).
8
+ *
9
+ * Instruments are created once — duplicate `createHistogram` calls on one
10
+ * meter log warnings, hence the module cache. Without an SDK these are
11
+ * noops. Attribute values stay low-cardinality (operations, statuses);
12
+ * IDs and paths never appear here.
13
+ */
14
+ /** Meter scope for StoryShelf instruments. */
15
+ const METER_NAME = "storyshelf";
16
+ let capture;
17
+ let database;
18
+ let storage;
19
+ /** Capture-pipeline instruments (created once). */
20
+ function captureMetrics() {
21
+ if (!capture) {
22
+ const meter = metrics.getMeter(METER_NAME);
23
+ capture = {
24
+ jobDuration: meter.createHistogram("capture.job.duration", { unit: "ms" }),
25
+ jobsCompleted: meter.createCounter("capture.jobs.completed"),
26
+ jobsFailed: meter.createCounter("capture.jobs.failed")
27
+ };
28
+ }
29
+ return capture;
30
+ }
31
+ /** Database-operation instruments (created once). */
32
+ function dbMetrics() {
33
+ if (!database) database = { operationDuration: metrics.getMeter(METER_NAME).createHistogram("db.operation.duration", { unit: "ms" }) };
34
+ return database;
35
+ }
36
+ /** Storage-operation instruments (created once). */
37
+ function storageMetrics() {
38
+ if (!storage) storage = { operationDuration: metrics.getMeter(METER_NAME).createHistogram("storage.operation.duration", { unit: "ms" }) };
39
+ return storage;
40
+ }
41
+ //#endregion
42
+ export { captureMetrics, dbMetrics, storageMetrics };
43
+
44
+ //# sourceMappingURL=metrics.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.mjs","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["/**\n * Memoized OTEL instruments (global meter, `storyshelf` scope).\n *\n * Instruments are created once — duplicate `createHistogram` calls on one\n * meter log warnings, hence the module cache. Without an SDK these are\n * noops. Attribute values stay low-cardinality (operations, statuses);\n * IDs and paths never appear here.\n */\nimport { metrics, type Counter, type Histogram } from \"@opentelemetry/api\";\n\n/** Meter scope for StoryShelf instruments. */\nconst METER_NAME = \"storyshelf\";\n\n/** Capture-pipeline instruments. */\nexport interface CaptureInstruments {\n /** Capture job wall duration in milliseconds (`capture.job.status`). */\n jobDuration: Histogram;\n /** Completed jobs (`capture.job.status = completed`). */\n jobsCompleted: Counter;\n /** Failed jobs (`capture.job.status = failed`). */\n jobsFailed: Counter;\n}\n\n/** Database-operation instruments. */\nexport interface DbInstruments {\n /** Per-operation wall duration in milliseconds (`db.operation`, `db.table`). */\n operationDuration: Histogram;\n}\n\n/** Storage-operation instruments. */\nexport interface StorageInstruments {\n /** Per-operation wall duration in milliseconds (`storage.operation`). */\n operationDuration: Histogram;\n}\n\nlet capture: CaptureInstruments | undefined;\nlet database: DbInstruments | undefined;\nlet storage: StorageInstruments | undefined;\n\n/** Capture-pipeline instruments (created once). */\nexport function captureMetrics(): CaptureInstruments {\n if (!capture) {\n const meter = metrics.getMeter(METER_NAME);\n capture = {\n jobDuration: meter.createHistogram(\"capture.job.duration\", { unit: \"ms\" }),\n jobsCompleted: meter.createCounter(\"capture.jobs.completed\"),\n jobsFailed: meter.createCounter(\"capture.jobs.failed\"),\n };\n }\n return capture;\n}\n\n/** Database-operation instruments (created once). */\nexport function dbMetrics(): DbInstruments {\n if (!database) {\n const meter = metrics.getMeter(METER_NAME);\n database = {\n operationDuration: meter.createHistogram(\"db.operation.duration\", { unit: \"ms\" }),\n };\n }\n return database;\n}\n\n/** Storage-operation instruments (created once). */\nexport function storageMetrics(): StorageInstruments {\n if (!storage) {\n const meter = metrics.getMeter(METER_NAME);\n storage = {\n operationDuration: meter.createHistogram(\"storage.operation.duration\", { unit: \"ms\" }),\n };\n }\n return storage;\n}\n"],"mappings":";;;;;;;;;;;;;;AAWA,MAAM,aAAa;AAwBnB,IAAI;AACJ,IAAI;AACJ,IAAI;;AAGJ,SAAgB,iBAAqC;CACnD,IAAI,CAAC,SAAS;EACZ,MAAM,QAAQ,QAAQ,SAAS,UAAU;EACzC,UAAU;GACR,aAAa,MAAM,gBAAgB,wBAAwB,EAAE,MAAM,KAAK,CAAC;GACzE,eAAe,MAAM,cAAc,wBAAwB;GAC3D,YAAY,MAAM,cAAc,qBAAqB;EACvD;CACF;CACA,OAAO;AACT;;AAGA,SAAgB,YAA2B;CACzC,IAAI,CAAC,UAEH,WAAW,EACT,mBAFY,QAAQ,SAAS,UAEN,CAAC,CAAC,gBAAgB,yBAAyB,EAAE,MAAM,KAAK,CAAC,EAClF;CAEF,OAAO;AACT;;AAGA,SAAgB,iBAAqC;CACnD,IAAI,CAAC,SAEH,UAAU,EACR,mBAFY,QAAQ,SAAS,UAEN,CAAC,CAAC,gBAAgB,8BAA8B,EAAE,MAAM,KAAK,CAAC,EACvF;CAEF,OAAO;AACT"}
@@ -0,0 +1,18 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { HttpMiddlewareOptions } from "./types.mjs";
4
+ import { MiddlewareHandler } from "hono";
5
+ //#region src/middleware.d.ts
6
+ /**
7
+ * Create the instrumenting middleware.
8
+ *
9
+ * Mount directly after Hono's `requestId()` so `storyshelf.req_id` resolves;
10
+ * `enduser.id` resolves when `storeScope` runs downstream.
11
+ *
12
+ * @param options - Service name/version for span resources.
13
+ * @returns Hono middleware producing one `http.server` span per request.
14
+ */
15
+ declare function createHttpMiddleware(options?: HttpMiddlewareOptions): MiddlewareHandler;
16
+ //#endregion
17
+ export { createHttpMiddleware };
18
+ //# sourceMappingURL=middleware.d.mts.map
@@ -0,0 +1,48 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { context, trace } from "@opentelemetry/api";
5
+ import { httpInstrumentationMiddleware } from "@hono/otel";
6
+ //#region src/middleware.ts
7
+ /**
8
+ * Hono HTTP middleware: `@hono/otel` server spans plus StoryShelf context.
9
+ *
10
+ * `@hono/otel` owns the `http.server` span (W3C extraction, route-template
11
+ * naming, status attributes, request-duration metrics). This wrapper adds
12
+ * `storyshelf.req_id` and `enduser.id` after downstream handlers run — still
13
+ * inside the span, since `@hono/otel` finalizes after `next()` returns.
14
+ */
15
+ /** Copy request/user ids from the Hono context onto the active server span. */
16
+ function enrichSpan(c) {
17
+ const span = trace.getSpan(context.active());
18
+ if (!span) return;
19
+ const reqId = c.get("requestId");
20
+ if (reqId) span.setAttribute("storyshelf.req_id", reqId);
21
+ const userId = c.get("userId");
22
+ if (userId) span.setAttribute("enduser.id", userId);
23
+ }
24
+ /**
25
+ * Create the instrumenting middleware.
26
+ *
27
+ * Mount directly after Hono's `requestId()` so `storyshelf.req_id` resolves;
28
+ * `enduser.id` resolves when `storeScope` runs downstream.
29
+ *
30
+ * @param options - Service name/version for span resources.
31
+ * @returns Hono middleware producing one `http.server` span per request.
32
+ */
33
+ function createHttpMiddleware(options = {}) {
34
+ const otel = httpInstrumentationMiddleware({
35
+ serviceName: options.serviceName ?? "storyshelf",
36
+ serviceVersion: options.serviceVersion
37
+ });
38
+ return async (c, next) => {
39
+ return await otel(c, async () => {
40
+ await next();
41
+ enrichSpan(c);
42
+ });
43
+ };
44
+ }
45
+ //#endregion
46
+ export { createHttpMiddleware };
47
+
48
+ //# sourceMappingURL=middleware.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,20 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { ObservabilityHandle } from "./types.mjs";
4
+ import { ObservabilityConfig, resolveObservabilityConfig } from "./config.mjs";
5
+ //#region src/node.d.ts
6
+ /**
7
+ * Initialize observability from the environment.
8
+ *
9
+ * Starts the Node SDK when `OTEL_EXPORTER_OTLP_ENDPOINT` (or
10
+ * `STORYSHELF_OTEL_ENABLED=true`) is set; otherwise returns a noop handle
11
+ * (also under Deno or `OTEL_SDK_DISABLED=true`). Call before
12
+ * `createShelfApp` and flush via the handle next to `app.lifecycle.teardown`.
13
+ *
14
+ * @param env - Environment record (defaults to `process.env`).
15
+ * @returns The SDK handle (check `enabled`, always call `shutdown`).
16
+ */
17
+ declare function initObservabilityFromEnv(env?: NodeJS.ProcessEnv): Promise<ObservabilityHandle>;
18
+ //#endregion
19
+ export { type ObservabilityConfig, type ObservabilityHandle, initObservabilityFromEnv, resolveObservabilityConfig };
20
+ //# sourceMappingURL=node.d.mts.map
package/dist/node.mjs ADDED
@@ -0,0 +1,83 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { resolveObservabilityConfig } from "./config.mjs";
5
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
6
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
+ import { resourceFromAttributes } from "@opentelemetry/resources";
8
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
9
+ import { NodeSDK } from "@opentelemetry/sdk-node";
10
+ import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
11
+ //#region src/node.ts
12
+ /**
13
+ * Node-only OTEL SDK lifecycle.
14
+ *
15
+ * Separate entrypoint (`@storyshelf/observability/node`) so the cross-runtime
16
+ * root never pulls Node SDK modules. Never import this file under Deno —
17
+ * `initObservabilityFromEnv` noops there and Deno's native OTEL owns export.
18
+ */
19
+ /** Noop handle: SDK disabled, nothing to flush. */
20
+ function noopHandle() {
21
+ return {
22
+ enabled: false,
23
+ shutdown: async () => {
24
+ await Promise.resolve();
25
+ }
26
+ };
27
+ }
28
+ /** OTLP/HTTP exporter options shared by trace and metric exporters. */
29
+ function exporterOptions(config) {
30
+ return {
31
+ url: config.endpoint ? `${config.endpoint}/v1/traces` : void 0,
32
+ headers: config.headers
33
+ };
34
+ }
35
+ /** Start the Node SDK wired to the OTLP/HTTP endpoint. */
36
+ function startSdk(config) {
37
+ const resource = resourceFromAttributes({
38
+ [ATTR_SERVICE_NAME]: config.serviceName,
39
+ ...config.serviceVersion ? { [ATTR_SERVICE_VERSION]: config.serviceVersion } : {},
40
+ ...process.env["NODE_ENV"] ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: process.env["NODE_ENV"] } : {}
41
+ });
42
+ const sdk = new NodeSDK({
43
+ resource,
44
+ traceExporter: new OTLPTraceExporter(exporterOptions(config)),
45
+ metricReaders: [new PeriodicExportingMetricReader({
46
+ exporter: new OTLPMetricExporter({
47
+ url: config.endpoint ? `${config.endpoint}/v1/metrics` : void 0,
48
+ headers: config.headers
49
+ }),
50
+ exportIntervalMillis: config.metricsIntervalMs
51
+ })]
52
+ });
53
+ sdk.start();
54
+ let shut = false;
55
+ return {
56
+ enabled: true,
57
+ shutdown: async () => {
58
+ if (shut) return;
59
+ shut = true;
60
+ await sdk.shutdown();
61
+ }
62
+ };
63
+ }
64
+ /**
65
+ * Initialize observability from the environment.
66
+ *
67
+ * Starts the Node SDK when `OTEL_EXPORTER_OTLP_ENDPOINT` (or
68
+ * `STORYSHELF_OTEL_ENABLED=true`) is set; otherwise returns a noop handle
69
+ * (also under Deno or `OTEL_SDK_DISABLED=true`). Call before
70
+ * `createShelfApp` and flush via the handle next to `app.lifecycle.teardown`.
71
+ *
72
+ * @param env - Environment record (defaults to `process.env`).
73
+ * @returns The SDK handle (check `enabled`, always call `shutdown`).
74
+ */
75
+ async function initObservabilityFromEnv(env = process.env) {
76
+ const config = resolveObservabilityConfig(env);
77
+ if (!config.enabled) return noopHandle();
78
+ return startSdk(config);
79
+ }
80
+ //#endregion
81
+ export { initObservabilityFromEnv, resolveObservabilityConfig };
82
+
83
+ //# sourceMappingURL=node.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.mjs","names":[],"sources":["../src/node.ts"],"sourcesContent":["/**\n * Node-only OTEL SDK lifecycle.\n *\n * Separate entrypoint (`@storyshelf/observability/node`) so the cross-runtime\n * root never pulls Node SDK modules. Never import this file under Deno —\n * `initObservabilityFromEnv` noops there and Deno's native OTEL owns export.\n */\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { PeriodicExportingMetricReader } from \"@opentelemetry/sdk-metrics\";\nimport { NodeSDK } from \"@opentelemetry/sdk-node\";\nimport {\n ATTR_DEPLOYMENT_ENVIRONMENT_NAME,\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport { resolveObservabilityConfig, type ObservabilityConfig } from \"./config.ts\";\nimport type { ObservabilityHandle } from \"./types.ts\";\n\n/** Noop handle: SDK disabled, nothing to flush. */\nfunction noopHandle(): ObservabilityHandle {\n return {\n enabled: false,\n shutdown: async (): Promise<void> => {\n await Promise.resolve();\n },\n };\n}\n\n/** OTLP/HTTP exporter options shared by trace and metric exporters. */\nfunction exporterOptions(config: ObservabilityConfig): {\n url?: string;\n headers?: Record<string, string>;\n} {\n return {\n url: config.endpoint ? `${config.endpoint}/v1/traces` : undefined,\n headers: config.headers,\n };\n}\n\n/** Start the Node SDK wired to the OTLP/HTTP endpoint. */\nfunction startSdk(config: ObservabilityConfig): ObservabilityHandle {\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: config.serviceName,\n ...(config.serviceVersion ? { [ATTR_SERVICE_VERSION]: config.serviceVersion } : {}),\n ...(process.env[\"NODE_ENV\"]\n ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: process.env[\"NODE_ENV\"] }\n : {}),\n });\n const sdk = new NodeSDK({\n resource,\n traceExporter: new OTLPTraceExporter(exporterOptions(config)),\n metricReaders: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: config.endpoint ? `${config.endpoint}/v1/metrics` : undefined,\n headers: config.headers,\n }),\n exportIntervalMillis: config.metricsIntervalMs,\n }),\n ],\n });\n sdk.start();\n let shut = false;\n return {\n enabled: true,\n shutdown: async (): Promise<void> => {\n if (shut) {\n return;\n }\n shut = true;\n await sdk.shutdown();\n },\n };\n}\n\n/**\n * Initialize observability from the environment.\n *\n * Starts the Node SDK when `OTEL_EXPORTER_OTLP_ENDPOINT` (or\n * `STORYSHELF_OTEL_ENABLED=true`) is set; otherwise returns a noop handle\n * (also under Deno or `OTEL_SDK_DISABLED=true`). Call before\n * `createShelfApp` and flush via the handle next to `app.lifecycle.teardown`.\n *\n * @param env - Environment record (defaults to `process.env`).\n * @returns The SDK handle (check `enabled`, always call `shutdown`).\n */\n// oxlint-disable-next-line typescript/require-await -- async signature; the disabled fast-path is synchronous\nexport async function initObservabilityFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Promise<ObservabilityHandle> {\n const config = resolveObservabilityConfig(env);\n if (!config.enabled) {\n return noopHandle();\n }\n return startSdk(config);\n}\n\nexport { resolveObservabilityConfig } from \"./config.ts\";\nexport type { ObservabilityConfig } from \"./config.ts\";\nexport type { ObservabilityHandle } from \"./types.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,SAAS,aAAkC;CACzC,OAAO;EACL,SAAS;EACT,UAAU,YAA2B;GACnC,MAAM,QAAQ,QAAQ;EACxB;CACF;AACF;;AAGA,SAAS,gBAAgB,QAGvB;CACA,OAAO;EACL,KAAK,OAAO,WAAW,GAAG,OAAO,SAAS,cAAc,KAAA;EACxD,SAAS,OAAO;CAClB;AACF;;AAGA,SAAS,SAAS,QAAkD;CAClE,MAAM,WAAW,uBAAuB;GACrC,oBAAoB,OAAO;EAC5B,GAAI,OAAO,iBAAiB,GAAG,uBAAuB,OAAO,eAAe,IAAI,CAAC;EACjF,GAAI,QAAQ,IAAI,cACZ,GAAG,mCAAmC,QAAQ,IAAI,YAAY,IAC9D,CAAC;CACP,CAAC;CACD,MAAM,MAAM,IAAI,QAAQ;EACtB;EACA,eAAe,IAAI,kBAAkB,gBAAgB,MAAM,CAAC;EAC5D,eAAe,CACb,IAAI,8BAA8B;GAChC,UAAU,IAAI,mBAAmB;IAC/B,KAAK,OAAO,WAAW,GAAG,OAAO,SAAS,eAAe,KAAA;IACzD,SAAS,OAAO;GAClB,CAAC;GACD,sBAAsB,OAAO;EAC/B,CAAC,CACH;CACF,CAAC;CACD,IAAI,MAAM;CACV,IAAI,OAAO;CACX,OAAO;EACL,SAAS;EACT,UAAU,YAA2B;GACnC,IAAI,MACF;GAEF,OAAO;GACP,MAAM,IAAI,SAAS;EACrB;CACF;AACF;;;;;;;;;;;;AAcA,eAAsB,yBACpB,MAAyB,QAAQ,KACH;CAC9B,MAAM,SAAS,2BAA2B,GAAG;CAC7C,IAAI,CAAC,OAAO,SACV,OAAO,WAAW;CAEpB,OAAO,SAAS,MAAM;AACxB"}
@@ -0,0 +1,24 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Context } from "@opentelemetry/api";
4
+ //#region src/propagate.d.ts
5
+ /** W3C traceparent header name. */
6
+ declare const TRACEPARENT_HEADER = "traceparent";
7
+ /**
8
+ * Resolve the parent context from an inbound `traceparent` value.
9
+ *
10
+ * @param traceparent - W3C value from a queue payload; undefined keeps context.
11
+ * @returns The extracted remote context, or the active context when absent.
12
+ */
13
+ declare function parentContext(traceparent?: string): Context;
14
+ /** Inject the active span context into a mutable header record. */
15
+ declare function injectTraceContext(headers: Record<string, string>): void;
16
+ /**
17
+ * Read the current W3C `traceparent` value for queue payloads.
18
+ *
19
+ * @returns The header value, or undefined when no span is active.
20
+ */
21
+ declare function currentTraceparent(): string | undefined;
22
+ //#endregion
23
+ export { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext };
24
+ //# sourceMappingURL=propagate.d.mts.map
@@ -0,0 +1,42 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { context, propagation } from "@opentelemetry/api";
5
+ //#region src/propagate.ts
6
+ /**
7
+ * W3C trace-context propagation helpers.
8
+ *
9
+ * Queue payloads carry the `traceparent` string (see `CaptureJob`) so a
10
+ * remote worker can continue the enqueueing request's trace; outbound HTTP
11
+ * injects the same header via {@link injectTraceContext}.
12
+ */
13
+ /** W3C traceparent header name. */
14
+ const TRACEPARENT_HEADER = "traceparent";
15
+ /**
16
+ * Resolve the parent context from an inbound `traceparent` value.
17
+ *
18
+ * @param traceparent - W3C value from a queue payload; undefined keeps context.
19
+ * @returns The extracted remote context, or the active context when absent.
20
+ */
21
+ function parentContext(traceparent) {
22
+ if (!traceparent) return context.active();
23
+ return propagation.extract(context.active(), { [TRACEPARENT_HEADER]: traceparent });
24
+ }
25
+ /** Inject the active span context into a mutable header record. */
26
+ function injectTraceContext(headers) {
27
+ propagation.inject(context.active(), headers);
28
+ }
29
+ /**
30
+ * Read the current W3C `traceparent` value for queue payloads.
31
+ *
32
+ * @returns The header value, or undefined when no span is active.
33
+ */
34
+ function currentTraceparent() {
35
+ const carrier = {};
36
+ propagation.inject(context.active(), carrier);
37
+ return carrier[TRACEPARENT_HEADER];
38
+ }
39
+ //#endregion
40
+ export { TRACEPARENT_HEADER, currentTraceparent, injectTraceContext, parentContext };
41
+
42
+ //# sourceMappingURL=propagate.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,20 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Attributes, Context, Span } from "@opentelemetry/api";
4
+ //#region src/tracing.d.ts
5
+ /**
6
+ * Run `fn` inside an internal span.
7
+ *
8
+ * On throw the span records the exception, is marked ERROR, and the error
9
+ * is rethrown — mirroring the pino `{ err }` convention.
10
+ *
11
+ * @param name - Span name (`domain.operation`, never a high-cardinality value).
12
+ * @param fn - Work to run with the span active.
13
+ * @param attributes - Fixed span attributes (no high-cardinality values in names).
14
+ * @param parent - Parent context (defaults to the active context).
15
+ * @returns Whatever `fn` returns.
16
+ */
17
+ declare function withSpan<T>(name: string, fn: (span: Span) => Promise<T>, attributes?: Attributes, parent?: Context): Promise<T>;
18
+ //#endregion
19
+ export { withSpan };
20
+ //# sourceMappingURL=tracing.d.mts.map
@@ -0,0 +1,44 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
5
+ //#region src/tracing.ts
6
+ /**
7
+ * Span helpers over `@opentelemetry/api` (runtime-agnostic: Node SDK or
8
+ * Deno's native bridge). Without a configured provider these are cheap
9
+ * noops, so call sites need no enabled-checks.
10
+ */
11
+ /** Instrumentation scope for observability-package spans. */
12
+ const SCOPE = "storyshelf.observability";
13
+ /**
14
+ * Run `fn` inside an internal span.
15
+ *
16
+ * On throw the span records the exception, is marked ERROR, and the error
17
+ * is rethrown — mirroring the pino `{ err }` convention.
18
+ *
19
+ * @param name - Span name (`domain.operation`, never a high-cardinality value).
20
+ * @param fn - Work to run with the span active.
21
+ * @param attributes - Fixed span attributes (no high-cardinality values in names).
22
+ * @param parent - Parent context (defaults to the active context).
23
+ * @returns Whatever `fn` returns.
24
+ */
25
+ async function withSpan(name, fn, attributes, parent) {
26
+ return await trace.getTracer(SCOPE).startActiveSpan(name, {
27
+ kind: SpanKind.INTERNAL,
28
+ attributes
29
+ }, parent ?? context.active(), async (span) => {
30
+ try {
31
+ return await fn(span);
32
+ } catch (error) {
33
+ span.recordException(error instanceof Error ? error : String(error));
34
+ span.setStatus({ code: SpanStatusCode.ERROR });
35
+ throw error;
36
+ } finally {
37
+ span.end();
38
+ }
39
+ });
40
+ }
41
+ //#endregion
42
+ export { withSpan };
43
+
44
+ //# sourceMappingURL=tracing.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,23 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Attributes } from "@opentelemetry/api";
4
+ //#region src/types.d.ts
5
+ /** Attributes attached to a span or metric measurement. */
6
+ type SpanAttributes = Attributes;
7
+ /** Shutdown handle for the observability SDK (see `node.ts`). */
8
+ interface ObservabilityHandle {
9
+ /** Whether the SDK is active (false = noop: no endpoint configured or Deno runtime). */
10
+ readonly enabled: boolean;
11
+ /** Flush exporters and release SDK resources. Idempotent. */
12
+ shutdown(): Promise<void>;
13
+ }
14
+ /** Options for the Hono HTTP middleware (`middleware.ts`). */
15
+ interface HttpMiddlewareOptions {
16
+ /** Service name in spans/metrics. Defaults to `OTEL_SERVICE_NAME` resolution. */
17
+ serviceName?: string;
18
+ /** Service version in spans/metrics. Defaults to the package version. */
19
+ serviceVersion?: string;
20
+ }
21
+ //#endregion
22
+ export { HttpMiddlewareOptions, ObservabilityHandle, SpanAttributes };
23
+ //# sourceMappingURL=types.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,96 @@
1
1
  {
2
2
  "name": "@storyshelf/observability",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.4",
4
+ "description": "StoryShelf observability: OpenTelemetry tracing, metrics, and log correlation over @storyshelf/core adapters.",
5
+ "keywords": [
6
+ "metrics",
7
+ "observability",
8
+ "opentelemetry",
9
+ "self-hosted",
10
+ "storybook",
11
+ "tracing",
12
+ "visual-testing"
13
+ ],
14
+ "homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": {
20
+ "name": "Siddhant Gupta",
21
+ "url": "https://guptasiddhant.com"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
26
+ "directory": "packages/observability"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "types": "./dist/index.d.mts",
34
+ "exports": {
35
+ ".": {
36
+ "source": "./src/index.ts",
37
+ "default": "./dist/index.mjs"
38
+ },
39
+ "./node": {
40
+ "source": "./src/node.ts",
41
+ "default": "./dist/node.mjs"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "publishConfig": {
46
+ "exports": {
47
+ ".": "./dist/index.mjs",
48
+ "./node": "./dist/node.mjs",
49
+ "./package.json": "./package.json"
50
+ },
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown",
55
+ "dev": "tsdown -w",
56
+ "fmt": "oxfmt ./src",
57
+ "lint": "oxlint --type-aware --type-check ./src",
58
+ "prepublishOnly": "nub run build",
59
+ "test": "vitest run"
60
+ },
61
+ "dependencies": {
62
+ "@hono/otel": "^1.1.2",
63
+ "@opentelemetry/api": "^1.9.0",
64
+ "@opentelemetry/resources": "^2.11.0",
65
+ "@opentelemetry/sdk-metrics": "^2.11.0",
66
+ "@opentelemetry/sdk-node": "^0.222.0",
67
+ "@opentelemetry/semantic-conventions": "^1.43.0",
68
+ "@storyshelf/core": "0.5.4",
69
+ "drizzle-orm": "^0.44.0",
70
+ "hono": "^4.11.0"
71
+ },
72
+ "devDependencies": {
73
+ "@opentelemetry/context-async-hooks": "^2.11.0",
74
+ "@opentelemetry/core": "^2.11.0",
75
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
76
+ "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
77
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
78
+ "@types/node": "^26.0.0",
79
+ "@vitest/coverage-v8": "^4.0.0",
80
+ "oxfmt": "^0.65.0",
81
+ "oxlint": "^1.80.0",
82
+ "oxlint-tsgolint": "^7.0.2001",
83
+ "tsdown": "^0.22.14",
84
+ "typescript": "^7.0.2",
85
+ "vitest": "^5.0.0"
86
+ },
87
+ "jsr": {
88
+ "runtimeCompat": {
89
+ "node": true,
90
+ "deno": true,
91
+ "bun": true,
92
+ "workerd": false,
93
+ "browser": false
94
+ }
95
+ }
96
+ }