@open-mercato/telemetry 0.6.7-develop.6834.1.e76f4b8cbc
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/.turbo/turbo-build.log +2 -0
- package/AGENTS.md +71 -0
- package/README.md +164 -0
- package/build.mjs +10 -0
- package/dist/env.js +44 -0
- package/dist/env.js.map +7 -0
- package/dist/facade/context.js +14 -0
- package/dist/facade/context.js.map +7 -0
- package/dist/facade/http.js +18 -0
- package/dist/facade/http.js.map +7 -0
- package/dist/facade/logger-bridge.js +40 -0
- package/dist/facade/logger-bridge.js.map +7 -0
- package/dist/facade/meter.js +16 -0
- package/dist/facade/meter.js.map +7 -0
- package/dist/facade/propagation.js +20 -0
- package/dist/facade/propagation.js.map +7 -0
- package/dist/facade/redact.js +21 -0
- package/dist/facade/redact.js.map +7 -0
- package/dist/facade/report-error.js +26 -0
- package/dist/facade/report-error.js.map +7 -0
- package/dist/facade/serialize.js +35 -0
- package/dist/facade/serialize.js.map +7 -0
- package/dist/facade/tracer.js +21 -0
- package/dist/facade/tracer.js.map +7 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +7 -0
- package/dist/init.js +98 -0
- package/dist/init.js.map +7 -0
- package/dist/nextjs-config.js +21 -0
- package/dist/nextjs-config.js.map +7 -0
- package/dist/nextjs.js +30 -0
- package/dist/nextjs.js.map +7 -0
- package/dist/provider/console-provider.js +70 -0
- package/dist/provider/console-provider.js.map +7 -0
- package/dist/provider/noop-provider.js +45 -0
- package/dist/provider/noop-provider.js.map +7 -0
- package/dist/provider/otlp-provider.js +212 -0
- package/dist/provider/otlp-provider.js.map +7 -0
- package/dist/provider/registry.js +40 -0
- package/dist/provider/registry.js.map +7 -0
- package/dist/provider/run-span.js +34 -0
- package/dist/provider/run-span.js.map +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +7 -0
- package/jest.config.cjs +27 -0
- package/package.json +68 -0
- package/src/__tests__/default-unloaded.test.ts +58 -0
- package/src/__tests__/env-load-order.test.ts +80 -0
- package/src/__tests__/env.test.ts +72 -0
- package/src/__tests__/logger-level.test.ts +64 -0
- package/src/__tests__/nextjs-shutdown.test.ts +71 -0
- package/src/__tests__/nextjs.test.ts +122 -0
- package/src/__tests__/otlp-integration.test.ts +264 -0
- package/src/__tests__/pg-instrumentation.test.ts +88 -0
- package/src/__tests__/pg-pii-config.test.ts +14 -0
- package/src/__tests__/redact.test.ts +92 -0
- package/src/__tests__/registry.test.ts +77 -0
- package/src/__tests__/run-span.test.ts +56 -0
- package/src/__tests__/serialize.test.ts +47 -0
- package/src/__tests__/telemetry.test.ts +172 -0
- package/src/env.ts +71 -0
- package/src/facade/context.ts +17 -0
- package/src/facade/http.ts +25 -0
- package/src/facade/logger-bridge.ts +50 -0
- package/src/facade/meter.ts +20 -0
- package/src/facade/propagation.ts +31 -0
- package/src/facade/redact.ts +54 -0
- package/src/facade/report-error.ts +41 -0
- package/src/facade/serialize.ts +46 -0
- package/src/facade/tracer.ts +25 -0
- package/src/index.ts +30 -0
- package/src/init.ts +134 -0
- package/src/nextjs-config.ts +22 -0
- package/src/nextjs.ts +47 -0
- package/src/provider/console-provider.ts +87 -0
- package/src/provider/noop-provider.ts +53 -0
- package/src/provider/otlp-provider.ts +316 -0
- package/src/provider/registry.ts +60 -0
- package/src/provider/run-span.ts +45 -0
- package/src/types.ts +108 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Telemetry Package — Agent Rules
|
|
2
|
+
|
|
3
|
+
`@open-mercato/telemetry` supplies vendor-neutral spans, metrics, error
|
|
4
|
+
reporting, and an optional remote sink for the canonical shared logger. It is
|
|
5
|
+
off by default. Spec:
|
|
6
|
+
`.ai/specs/2026-04-29-telemetry-and-otel.md`.
|
|
7
|
+
|
|
8
|
+
## Always
|
|
9
|
+
|
|
10
|
+
- Use `createLogger(namespace)` from
|
|
11
|
+
`@open-mercato/shared/lib/logger` for operational logging. Telemetry must
|
|
12
|
+
extend that logger, never introduce another logger or stdout/stderr path.
|
|
13
|
+
- Keep host integration default-unloaded: check
|
|
14
|
+
`isTelemetryBackendEnabled()` from shared code before dynamically importing
|
|
15
|
+
this package.
|
|
16
|
+
- Treat an unset, `noop`, or unregistered backend as absolute off. A custom
|
|
17
|
+
provider activates only after an explicit bootstrap registers the exact
|
|
18
|
+
configured name and calls `initTelemetry()`.
|
|
19
|
+
- Name spans `module.entity.action` (lowercase, dot-separated).
|
|
20
|
+
- Use semantic-convention metric/attribute names when available.
|
|
21
|
+
- Keep metric labels low-cardinality. Tenant, organization, and user IDs belong
|
|
22
|
+
on span attributes, never metric labels.
|
|
23
|
+
- Apply redaction at the provider boundary as well as at facade call sites.
|
|
24
|
+
|
|
25
|
+
## Ask First
|
|
26
|
+
|
|
27
|
+
- Ask before adding a built-in metric, auto-instrumentation, production
|
|
28
|
+
dependency, or new global hook.
|
|
29
|
+
- OpenTelemetry packages must stay optional and may only be imported by
|
|
30
|
+
`provider/otlp-provider.ts`.
|
|
31
|
+
- Ask before changing the `pg` `enhancedDatabaseReporting: false` guard or
|
|
32
|
+
broadening the accepted inbound-trace trust model.
|
|
33
|
+
|
|
34
|
+
## Never
|
|
35
|
+
|
|
36
|
+
- Never emit PII, credentials, record content, SQL parameters, request bodies,
|
|
37
|
+
or arbitrary thrown-object properties.
|
|
38
|
+
- Never trust `traceparent` or `x-original-traceparent` at an inbound/global
|
|
39
|
+
boundary unless `TELEMETRY_TRUST_INBOUND_TRACE=true`.
|
|
40
|
+
- Never store provider, shared-logger extension, or runtime bridge state only in
|
|
41
|
+
a module local; cross-bundle state uses `globalThis` symbol registries.
|
|
42
|
+
- Never replace provider-owned span delegation with a finished-span sink.
|
|
43
|
+
- Never import this package from a client component.
|
|
44
|
+
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
@open-mercato/shared/lib/logger ── local output (always)
|
|
49
|
+
│
|
|
50
|
+
└─ process-wide extension (only after telemetry init) ── remote logs
|
|
51
|
+
|
|
52
|
+
host/queue shared runtime bridge ── absent while off
|
|
53
|
+
│
|
|
54
|
+
└─ registered provider: console | OTLP
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `src/facade/*`: spans, metrics, propagation, error funnel, redaction, and
|
|
58
|
+
shared-logger adapter.
|
|
59
|
+
- `src/provider/*`: noop/console/OTLP providers and global provider registry.
|
|
60
|
+
- `src/init.ts`: explicit-enabled initialization and process-wide bridge
|
|
61
|
+
registration.
|
|
62
|
+
- `src/nextjs-config.ts`: build-time constants only; no runtime imports.
|
|
63
|
+
- `src/nextjs.ts`: enabled runtime helper.
|
|
64
|
+
|
|
65
|
+
## Validation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
yarn workspace @open-mercato/telemetry build
|
|
69
|
+
yarn workspace @open-mercato/telemetry test
|
|
70
|
+
yarn typecheck
|
|
71
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @open-mercato/telemetry
|
|
2
|
+
|
|
3
|
+
Vendor-neutral tracing, metrics, log export, and error reporting for Open
|
|
4
|
+
Mercato, backed by a pluggable provider with OpenTelemetry (OTLP) as the default
|
|
5
|
+
transport.
|
|
6
|
+
|
|
7
|
+
Telemetry is explicitly opt-in. The platform checks `TELEMETRY_BACKEND` from
|
|
8
|
+
shared code before importing this package, so the telemetry runtime is not
|
|
9
|
+
evaluated when the variable is unset, `noop`, or unknown. OpenTelemetry packages
|
|
10
|
+
are optional dependencies and are dynamically imported only for an OTLP backend.
|
|
11
|
+
Package managers may still install optional dependencies; the guarantee is zero
|
|
12
|
+
disabled-path runtime loading, hook registration, or export traffic.
|
|
13
|
+
|
|
14
|
+
See [the telemetry spec](../../.ai/specs/2026-04-29-telemetry-and-otel.md).
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
The canonical application logger remains
|
|
19
|
+
`@open-mercato/shared/lib/logger`. Telemetry extends it after successful
|
|
20
|
+
initialization with trace correlation and one remote sink; it does not create a
|
|
21
|
+
second logger or local output path.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
25
|
+
import { withSpan, counter, reportError } from '@open-mercato/telemetry'
|
|
26
|
+
|
|
27
|
+
const log = createLogger('orders').child({ module: 'sales' })
|
|
28
|
+
log.info('Order placed', { orderId })
|
|
29
|
+
|
|
30
|
+
await withSpan('orders.checkout', async (span) => {
|
|
31
|
+
span.setAttribute('om.tenant_id', tenantId)
|
|
32
|
+
// pg/undici auto-spans nest here when OTLP is enabled
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
counter('om.errors', 1, { module: 'orders' })
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// ...
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reportError(error, { module: 'orders' })
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Use `OM_LOG_LEVEL`, `OM_LOG_PRETTY`, and `OM_LOG_DESTINATION` for both the
|
|
46
|
+
normal local logger and telemetry-backed log export. Remote records follow the
|
|
47
|
+
same shared level gate as local records.
|
|
48
|
+
|
|
49
|
+
## Enabling
|
|
50
|
+
|
|
51
|
+
Leave `TELEMETRY_BACKEND` unset (or set it to `noop`) to keep telemetry off.
|
|
52
|
+
Unknown values also resolve to off.
|
|
53
|
+
|
|
54
|
+
```dotenv
|
|
55
|
+
TELEMETRY_BACKEND=otlp
|
|
56
|
+
TELEMETRY_SAMPLING_RATIO=0.1
|
|
57
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-collector>:4318
|
|
58
|
+
OTEL_EXPORTER_OTLP_HEADERS=<auth-header>=<key>
|
|
59
|
+
OTEL_SERVICE_NAME=open-mercato
|
|
60
|
+
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`otlp`, `signoz`, and `newrelic` select the same OTLP provider and differ
|
|
64
|
+
only by endpoint and headers. `console` is an explicit local span/metric
|
|
65
|
+
diagnostic backend.
|
|
66
|
+
|
|
67
|
+
`TELEMETRY_TRUST_INBOUND_TRACE` defaults to false. In that mode both
|
|
68
|
+
`traceparent` and `x-original-traceparent` from inbound/global carriers are
|
|
69
|
+
ignored because either header is caller-controlled. Set it to true only behind a
|
|
70
|
+
trusted upstream when global W3C continuation is required. The queue package's
|
|
71
|
+
dedicated `metadata._trace` carrier does not require this flag. The richer
|
|
72
|
+
`bullmq-otel` queue spans do use the process-global propagator, so they stay
|
|
73
|
+
disabled while this flag is false; async queues then use the dedicated carrier.
|
|
74
|
+
|
|
75
|
+
Custom providers remain supported through `registerProvider()`. Because the
|
|
76
|
+
default host guard deliberately imports telemetry only for built-in backend
|
|
77
|
+
names, a custom-provider bootstrap must import `@open-mercato/telemetry`,
|
|
78
|
+
register a provider whose `name` matches `TELEMETRY_BACKEND`, and call
|
|
79
|
+
`initTelemetry()` directly. Unregistered names remain a hard no-op.
|
|
80
|
+
|
|
81
|
+
## Existing apps
|
|
82
|
+
|
|
83
|
+
Fresh create-app scaffolds are already wired. Older apps can run:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
yarn mercato telemetry init
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The command is idempotent and supports `--dry-run`. The equivalent manual
|
|
90
|
+
wiring is:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// src/instrumentation.ts
|
|
94
|
+
import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
95
|
+
|
|
96
|
+
export async function register(): Promise<void> {
|
|
97
|
+
if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {
|
|
98
|
+
const { registerTelemetryForNextjs } =
|
|
99
|
+
await import('@open-mercato/telemetry/nextjs')
|
|
100
|
+
await registerTelemetryForNextjs()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// next.config.ts — config-only entrypoint, no telemetry runtime imports
|
|
107
|
+
import { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// shared API dispatcher
|
|
112
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
113
|
+
|
|
114
|
+
getTelemetryRuntime()?.recordHttpDuration(method, route.path, status, startedAt)
|
|
115
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
116
|
+
attributes: { 'http.route': route.path },
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The CLI and queue worker use the same explicit-backend check before their dynamic
|
|
121
|
+
telemetry import. Queue enqueue/dispatch code talks only to the shared runtime
|
|
122
|
+
bridge, so the package is not loaded on the disabled path.
|
|
123
|
+
|
|
124
|
+
## Public API
|
|
125
|
+
|
|
126
|
+
| Export | Description |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `withSpan(name, fn, opts?)` | Run `fn` in a provider-owned span |
|
|
129
|
+
| `currentSpan()` / `setAttributes(attrs)` | Active span access |
|
|
130
|
+
| `counter` / `histogram` / `gauge` | Metric helpers |
|
|
131
|
+
| `reportError(err, ctx?)` | Span exception + shared error log + `om.errors` |
|
|
132
|
+
| `captureTraceContext()` / `continueTrace(...)` | Dedicated cross-boundary propagation |
|
|
133
|
+
| `initTelemetry()` / `shutdownTelemetry()` | Opt-in bootstrap and flush |
|
|
134
|
+
| `registerProvider(provider)` | Register a custom provider for an enabled backend name |
|
|
135
|
+
|
|
136
|
+
`@open-mercato/telemetry/nextjs` exports the runtime
|
|
137
|
+
`registerTelemetryForNextjs()` and `recordHttpDuration()` helpers.
|
|
138
|
+
`@open-mercato/telemetry/nextjs-config` separately exports only
|
|
139
|
+
`telemetryServerExternalPackages` for build configuration.
|
|
140
|
+
|
|
141
|
+
## Security and privacy
|
|
142
|
+
|
|
143
|
+
- Explicit off is absolute: custom providers cannot override an unset/noop
|
|
144
|
+
backend, and no process-wide logger/runtime hooks are registered.
|
|
145
|
+
- Secret-looking attribute keys (including exact `token`) are masked, while
|
|
146
|
+
benign keys such as `token_count` remain intact.
|
|
147
|
+
- Error serialization includes only name, message, and stack. Arbitrary thrown
|
|
148
|
+
objects are never JSON-stringified.
|
|
149
|
+
- Redaction runs again at the OTLP provider boundary for log bodies, error
|
|
150
|
+
fields, span attributes, and metric labels.
|
|
151
|
+
- Postgres parameter-value capture is locked off with
|
|
152
|
+
`enhancedDatabaseReporting: false`.
|
|
153
|
+
- Metric labels must remain low-cardinality and must never contain tenant,
|
|
154
|
+
organization, or user IDs.
|
|
155
|
+
|
|
156
|
+
The package is server-only because span context uses `node:async_hooks`.
|
|
157
|
+
|
|
158
|
+
## Validation
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
yarn workspace @open-mercato/telemetry build
|
|
162
|
+
yarn workspace @open-mercato/telemetry test
|
|
163
|
+
yarn typecheck
|
|
164
|
+
```
|
package/build.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { buildPackage } from '../../scripts/build-package.mjs'
|
|
4
|
+
|
|
5
|
+
const packageDir = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
|
|
7
|
+
await buildPackage(packageDir, {
|
|
8
|
+
name: 'telemetry',
|
|
9
|
+
entryPoints: 'src/**/*.ts',
|
|
10
|
+
})
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isTelemetryBackendEnabled } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
2
|
+
const OTLP_BACKENDS = ["signoz", "newrelic", "otlp"];
|
|
3
|
+
function isOtlpBackend(name) {
|
|
4
|
+
return OTLP_BACKENDS.includes(name);
|
|
5
|
+
}
|
|
6
|
+
function parseBackend(raw) {
|
|
7
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
8
|
+
return isTelemetryBackendEnabled(v) ? v : "noop";
|
|
9
|
+
}
|
|
10
|
+
function parseSampling(raw, isProd) {
|
|
11
|
+
const n = Number.parseFloat((raw ?? "").trim());
|
|
12
|
+
if (Number.isFinite(n) && n >= 0 && n <= 1) return n;
|
|
13
|
+
return isProd ? 0.1 : 1;
|
|
14
|
+
}
|
|
15
|
+
function parseBool(raw, fallback) {
|
|
16
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
17
|
+
if (v === "true" || v === "1") return true;
|
|
18
|
+
if (v === "false" || v === "0") return false;
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
let cached;
|
|
22
|
+
function readTelemetryEnv() {
|
|
23
|
+
if (cached) return cached;
|
|
24
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
25
|
+
const backend = parseBackend(process.env.TELEMETRY_BACKEND);
|
|
26
|
+
cached = {
|
|
27
|
+
backend,
|
|
28
|
+
enabled: backend !== "noop",
|
|
29
|
+
samplingRatio: parseSampling(process.env.TELEMETRY_SAMPLING_RATIO, isProd),
|
|
30
|
+
trustInboundTrace: parseBool(process.env.TELEMETRY_TRUST_INBOUND_TRACE, false),
|
|
31
|
+
serviceName: process.env.OTEL_SERVICE_NAME?.trim() || "open-mercato",
|
|
32
|
+
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || void 0
|
|
33
|
+
};
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
function resetTelemetryEnvCache() {
|
|
37
|
+
cached = void 0;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
isOtlpBackend,
|
|
41
|
+
readTelemetryEnv,
|
|
42
|
+
resetTelemetryEnvCache
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=env.js.map
|
package/dist/env.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/env.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TelemetryBackendName } from './types'\nimport { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n\n/** Backend names that resolve to the OTLP provider (vendor differs only by endpoint). */\nconst OTLP_BACKENDS: readonly TelemetryBackendName[] = ['signoz', 'newrelic', 'otlp']\nexport function isOtlpBackend(name: TelemetryBackendName): boolean {\n return (OTLP_BACKENDS as readonly string[]).includes(name)\n}\n\nexport type TelemetryEnv = {\n /** Active backend. Unset/unknown \u2192 'noop' (off). */\n backend: TelemetryBackendName\n /** True unless backend is 'noop'. */\n enabled: boolean\n /** 0.0\u20131.0 trace sampling ratio. */\n samplingRatio: number\n /**\n * Continue the inbound HTTP trace (standard W3C extract) instead of rooting\n * per request. Default false. Set true only when embedded behind a trusted\n * upstream whose trace should continue.\n */\n trustInboundTrace: boolean\n serviceName: string\n /** OTLP endpoint (provider reads standard OTEL vars; surfaced here for diagnostics). */\n otlpEndpoint?: string\n}\n\nfunction parseBackend(raw: string | undefined): TelemetryBackendName {\n const v = (raw ?? '').trim().toLowerCase()\n return isTelemetryBackendEnabled(v) ? (v as TelemetryBackendName) : 'noop'\n}\n\nfunction parseSampling(raw: string | undefined, isProd: boolean): number {\n const n = Number.parseFloat((raw ?? '').trim())\n if (Number.isFinite(n) && n >= 0 && n <= 1) return n\n return isProd ? 0.1 : 1.0\n}\n\n/** Parse a boolean env flag; unset/unknown \u2192 `fallback`. */\nfunction parseBool(raw: string | undefined, fallback: boolean): boolean {\n const v = (raw ?? '').trim().toLowerCase()\n if (v === 'true' || v === '1') return true\n if (v === 'false' || v === '0') return false\n return fallback\n}\n\nlet cached: TelemetryEnv | undefined\n\nexport function readTelemetryEnv(): TelemetryEnv {\n if (cached) return cached\n const isProd = process.env.NODE_ENV === 'production'\n const backend = parseBackend(process.env.TELEMETRY_BACKEND)\n cached = {\n backend,\n enabled: backend !== 'noop',\n samplingRatio: parseSampling(process.env.TELEMETRY_SAMPLING_RATIO, isProd),\n trustInboundTrace: parseBool(process.env.TELEMETRY_TRUST_INBOUND_TRACE, false),\n serviceName: process.env.OTEL_SERVICE_NAME?.trim() || 'open-mercato',\n otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || undefined,\n }\n return cached\n}\n\n/**\n * Reset the memoized env snapshot. Called by `initTelemetry()` so init always\n * resolves from the fully-loaded environment (a host may import this package\n * before its `.env` is loaded \u2014 the CLI binary does), and by jest between tests.\n */\nexport function resetTelemetryEnvCache(): void {\n cached = undefined\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,iCAAiC;AAG1C,MAAM,gBAAiD,CAAC,UAAU,YAAY,MAAM;AAC7E,SAAS,cAAc,MAAqC;AACjE,SAAQ,cAAoC,SAAS,IAAI;AAC3D;AAoBA,SAAS,aAAa,KAA+C;AACnE,QAAM,KAAK,OAAO,IAAI,KAAK,EAAE,YAAY;AACzC,SAAO,0BAA0B,CAAC,IAAK,IAA6B;AACtE;AAEA,SAAS,cAAc,KAAyB,QAAyB;AACvE,QAAM,IAAI,OAAO,YAAY,OAAO,IAAI,KAAK,CAAC;AAC9C,MAAI,OAAO,SAAS,CAAC,KAAK,KAAK,KAAK,KAAK,EAAG,QAAO;AACnD,SAAO,SAAS,MAAM;AACxB;AAGA,SAAS,UAAU,KAAyB,UAA4B;AACtE,QAAM,KAAK,OAAO,IAAI,KAAK,EAAE,YAAY;AACzC,MAAI,MAAM,UAAU,MAAM,IAAK,QAAO;AACtC,MAAI,MAAM,WAAW,MAAM,IAAK,QAAO;AACvC,SAAO;AACT;AAEA,IAAI;AAEG,SAAS,mBAAiC;AAC/C,MAAI,OAAQ,QAAO;AACnB,QAAM,SAAS,QAAQ,IAAI,aAAa;AACxC,QAAM,UAAU,aAAa,QAAQ,IAAI,iBAAiB;AAC1D,WAAS;AAAA,IACP;AAAA,IACA,SAAS,YAAY;AAAA,IACrB,eAAe,cAAc,QAAQ,IAAI,0BAA0B,MAAM;AAAA,IACzE,mBAAmB,UAAU,QAAQ,IAAI,+BAA+B,KAAK;AAAA,IAC7E,aAAa,QAAQ,IAAI,mBAAmB,KAAK,KAAK;AAAA,IACtD,cAAc,QAAQ,IAAI,6BAA6B,KAAK,KAAK;AAAA,EACnE;AACA,SAAO;AACT;AAOO,SAAS,yBAA+B;AAC7C,WAAS;AACX;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const spanStore = new AsyncLocalStorage();
|
|
3
|
+
function runWithSpan(span, fn) {
|
|
4
|
+
return spanStore.run(span, fn);
|
|
5
|
+
}
|
|
6
|
+
function spanFromStore() {
|
|
7
|
+
return spanStore.getStore();
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
runWithSpan,
|
|
11
|
+
spanFromStore,
|
|
12
|
+
spanStore
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/context.ts"],
|
|
4
|
+
"sourcesContent": ["import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { Span } from '../types'\n\n/**\n * Carries the facade's active span across `await` boundaries for spans created\n * via `withSpan`. (OTEL auto-instrumentation spans live in OTEL's own context;\n * the OTLP provider's `activeSpan()` bridges to those \u2014 see `currentSpan`.)\n */\nexport const spanStore = new AsyncLocalStorage<Span>()\n\nexport function runWithSpan<T>(span: Span, fn: () => T): T {\n return spanStore.run(span, fn)\n}\n\nexport function spanFromStore(): Span | undefined {\n return spanStore.getStore()\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAyB;AAQ3B,MAAM,YAAY,IAAI,kBAAwB;AAE9C,SAAS,YAAe,MAAY,IAAgB;AACzD,SAAO,UAAU,IAAI,MAAM,EAAE;AAC/B;AAEO,SAAS,gBAAkC;AAChD,SAAO,UAAU,SAAS;AAC5B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { histogram } from "./meter.js";
|
|
2
|
+
function recordHttpDuration(method, route, status, startedAt) {
|
|
3
|
+
histogram(
|
|
4
|
+
"http.server.request.duration",
|
|
5
|
+
(Date.now() - startedAt) / 1e3,
|
|
6
|
+
{
|
|
7
|
+
"http.request.method": method,
|
|
8
|
+
"http.route": route,
|
|
9
|
+
"http.response.status_code": status,
|
|
10
|
+
"error.type": status >= 500 ? String(status) : void 0
|
|
11
|
+
},
|
|
12
|
+
"s"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
recordHttpDuration
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/http.ts"],
|
|
4
|
+
"sourcesContent": ["import { histogram } from './meter'\n\n/**\n * Emit the OpenTelemetry-standard HTTP server metric\n * `http.server.request.duration` (histogram, seconds) with semconv attributes.\n * `route` MUST be a low-cardinality route template, never a resolved pathname.\n */\nexport function recordHttpDuration(\n method: string,\n route: string,\n status: number,\n startedAt: number,\n): void {\n histogram(\n 'http.server.request.duration',\n (Date.now() - startedAt) / 1000,\n {\n 'http.request.method': method,\n 'http.route': route,\n 'http.response.status_code': status,\n 'error.type': status >= 500 ? String(status) : undefined,\n },\n 's',\n )\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAOnB,SAAS,mBACd,QACA,OACA,QACA,WACM;AACN;AAAA,IACE;AAAA,KACC,KAAK,IAAI,IAAI,aAAa;AAAA,IAC3B;AAAA,MACE,uBAAuB;AAAA,MACvB,cAAc;AAAA,MACd,6BAA6B;AAAA,MAC7B,cAAc,UAAU,MAAM,OAAO,MAAM,IAAI;AAAA,IACjD;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerLoggerExtension
|
|
3
|
+
} from "@open-mercato/shared/lib/logger";
|
|
4
|
+
import { redactAttributes } from "./redact.js";
|
|
5
|
+
import { serializeError } from "./serialize.js";
|
|
6
|
+
function toAttributes(record) {
|
|
7
|
+
const attributes = {
|
|
8
|
+
"logger.name": record.namespace
|
|
9
|
+
};
|
|
10
|
+
for (const [key, value] of Object.entries(record.fields)) {
|
|
11
|
+
if (key === "err" || value === void 0) continue;
|
|
12
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
13
|
+
attributes[key] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return redactAttributes(attributes);
|
|
17
|
+
}
|
|
18
|
+
function toLogRecord(record) {
|
|
19
|
+
const error = record.fields.err === void 0 ? void 0 : serializeError(record.fields.err);
|
|
20
|
+
return {
|
|
21
|
+
level: record.level,
|
|
22
|
+
message: record.message,
|
|
23
|
+
attributes: toAttributes(record),
|
|
24
|
+
error,
|
|
25
|
+
time: record.time
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function registerTelemetryLogger(provider) {
|
|
29
|
+
return registerLoggerExtension({
|
|
30
|
+
enrich: () => {
|
|
31
|
+
const context = provider.activeTraceContext();
|
|
32
|
+
return context ? { trace_id: context.traceId, span_id: context.spanId } : void 0;
|
|
33
|
+
},
|
|
34
|
+
emit: (record) => provider.emitLog(toLogRecord(record))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
registerTelemetryLogger
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=logger-bridge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/logger-bridge.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n registerLoggerExtension,\n type LoggerExtensionRecord,\n} from '@open-mercato/shared/lib/logger'\nimport type { Attributes, LogRecord, TelemetryProvider } from '../types'\nimport { redactAttributes } from './redact'\nimport { serializeError } from './serialize'\n\nfunction toAttributes(record: LoggerExtensionRecord): Attributes {\n const attributes: Attributes = {\n 'logger.name': record.namespace,\n }\n for (const [key, value] of Object.entries(record.fields)) {\n if (key === 'err' || value === undefined) continue\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n attributes[key] = value\n }\n }\n return redactAttributes(attributes)\n}\n\nfunction toLogRecord(record: LoggerExtensionRecord): LogRecord {\n const error = record.fields.err === undefined\n ? undefined\n : serializeError(record.fields.err)\n return {\n level: record.level,\n message: record.message,\n attributes: toAttributes(record),\n error,\n time: record.time,\n }\n}\n\n/**\n * Extend the canonical shared logger with trace correlation + remote export.\n * The shared logger remains the sole application-facing logger and sole local\n * output path; telemetry observes the already-normalized record exactly once.\n */\nexport function registerTelemetryLogger(provider: TelemetryProvider): () => void {\n return registerLoggerExtension({\n enrich: () => {\n const context = provider.activeTraceContext()\n return context\n ? { trace_id: context.traceId, span_id: context.spanId }\n : undefined\n },\n emit: (record) => provider.emitLog(toLogRecord(record)),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,OAEK;AAEP,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAE/B,SAAS,aAAa,QAA2C;AAC/D,QAAM,aAAyB;AAAA,IAC7B,eAAe,OAAO;AAAA,EACxB;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,MAAM,GAAG;AACxD,QAAI,QAAQ,SAAS,UAAU,OAAW;AAC1C,QAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AACxF,iBAAW,GAAG,IAAI;AAAA,IACpB;AAAA,EACF;AACA,SAAO,iBAAiB,UAAU;AACpC;AAEA,SAAS,YAAY,QAA0C;AAC7D,QAAM,QAAQ,OAAO,OAAO,QAAQ,SAChC,SACA,eAAe,OAAO,OAAO,GAAG;AACpC,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,SAAS,OAAO;AAAA,IAChB,YAAY,aAAa,MAAM;AAAA,IAC/B;AAAA,IACA,MAAM,OAAO;AAAA,EACf;AACF;AAOO,SAAS,wBAAwB,UAAyC;AAC/E,SAAO,wBAAwB;AAAA,IAC7B,QAAQ,MAAM;AACZ,YAAM,UAAU,SAAS,mBAAmB;AAC5C,aAAO,UACH,EAAE,UAAU,QAAQ,SAAS,SAAS,QAAQ,OAAO,IACrD;AAAA,IACN;AAAA,IACA,MAAM,CAAC,WAAW,SAAS,QAAQ,YAAY,MAAM,CAAC;AAAA,EACxD,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getActiveProvider } from "../provider/registry.js";
|
|
2
|
+
function counter(name, value = 1, labels, unit) {
|
|
3
|
+
getActiveProvider().recordMetric({ kind: "counter", name, value, labels, unit });
|
|
4
|
+
}
|
|
5
|
+
function histogram(name, value, labels, unit) {
|
|
6
|
+
getActiveProvider().recordMetric({ kind: "histogram", name, value, labels, unit });
|
|
7
|
+
}
|
|
8
|
+
function gauge(name, value, labels, unit) {
|
|
9
|
+
getActiveProvider().recordMetric({ kind: "gauge", name, value, labels, unit });
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
counter,
|
|
13
|
+
gauge,
|
|
14
|
+
histogram
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=meter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/meter.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Attributes } from '../types'\nimport { getActiveProvider } from '../provider/registry'\n\n/**\n * Metric helpers. Labels MUST be low-cardinality (never tenant/org/user IDs \u2014\n * those belong on span attributes, per the telemetry spec's R4). The active\n * provider caches the underlying instrument by name; these are no-ops when\n * metrics are off.\n */\nexport function counter(name: string, value = 1, labels?: Attributes, unit?: string): void {\n getActiveProvider().recordMetric({ kind: 'counter', name, value, labels, unit })\n}\n\nexport function histogram(name: string, value: number, labels?: Attributes, unit?: string): void {\n getActiveProvider().recordMetric({ kind: 'histogram', name, value, labels, unit })\n}\n\nexport function gauge(name: string, value: number, labels?: Attributes, unit?: string): void {\n getActiveProvider().recordMetric({ kind: 'gauge', name, value, labels, unit })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAQ3B,SAAS,QAAQ,MAAc,QAAQ,GAAG,QAAqB,MAAqB;AACzF,oBAAkB,EAAE,aAAa,EAAE,MAAM,WAAW,MAAM,OAAO,QAAQ,KAAK,CAAC;AACjF;AAEO,SAAS,UAAU,MAAc,OAAe,QAAqB,MAAqB;AAC/F,oBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,MAAM,OAAO,QAAQ,KAAK,CAAC;AACnF;AAEO,SAAS,MAAM,MAAc,OAAe,QAAqB,MAAqB;AAC3F,oBAAkB,EAAE,aAAa,EAAE,MAAM,SAAS,MAAM,OAAO,QAAQ,KAAK,CAAC;AAC/E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getActiveProvider } from "../provider/registry.js";
|
|
2
|
+
import { runWithSpan } from "./context.js";
|
|
3
|
+
function captureTraceContext() {
|
|
4
|
+
const carrier = {};
|
|
5
|
+
getActiveProvider().inject(carrier);
|
|
6
|
+
return carrier;
|
|
7
|
+
}
|
|
8
|
+
function continueTrace(carrier, name, fn, options) {
|
|
9
|
+
return getActiveProvider().runInRemoteSpan(
|
|
10
|
+
carrier ?? {},
|
|
11
|
+
name,
|
|
12
|
+
options ?? {},
|
|
13
|
+
(span) => runWithSpan(span, () => fn(span))
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
captureTraceContext,
|
|
18
|
+
continueTrace
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=propagation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/propagation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Span, SpanOptions, TraceCarrier } from '../types'\nimport { getActiveProvider } from '../provider/registry'\nimport { runWithSpan } from './context'\n\n/**\n * Capture the active trace context into a fresh carrier, to embed in a queue job\n * or event payload (the queue/event bus expose no metadata channel \u2014 see the\n * telemetry spec's S5). Returns `{}` when telemetry is off.\n */\nexport function captureTraceContext(): TraceCarrier {\n const carrier: TraceCarrier = {}\n getActiveProvider().inject(carrier)\n return carrier\n}\n\n/**\n * Continue a trace on the consumer side (worker / subscriber): run `fn` inside a\n * new active span named `name`, parented to the context in `carrier`. The job's\n * spans (pg, etc.) then nest under the producer's trace as one waterfall. If\n * `carrier` is empty/undefined the span simply starts a fresh root.\n */\nexport function continueTrace<T>(\n carrier: TraceCarrier | undefined,\n name: string,\n fn: (span: Span) => T,\n options?: SpanOptions,\n): T {\n return getActiveProvider().runInRemoteSpan(carrier ?? {}, name, options ?? {}, (span) =>\n runWithSpan(span, () => fn(span)),\n )\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAOrB,SAAS,sBAAoC;AAClD,QAAM,UAAwB,CAAC;AAC/B,oBAAkB,EAAE,OAAO,OAAO;AAClC,SAAO;AACT;AAQO,SAAS,cACd,SACA,MACA,IACA,SACG;AACH,SAAO,kBAAkB,EAAE;AAAA,IAAgB,WAAW,CAAC;AAAA,IAAG;AAAA,IAAM,WAAW,CAAC;AAAA,IAAG,CAAC,SAC9E,YAAY,MAAM,MAAM,GAAG,IAAI,CAAC;AAAA,EAClC;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const EMAIL_RE = /[A-Z0-9._%+-]{1,64}@[A-Z0-9.-]{1,253}\.[A-Z]{2,63}/gi;
|
|
2
|
+
const SECRET_HEADER_RE = /\b(authorization|proxy-authorization|cookie|set-cookie)(\s*[:=]\s*)([^\n;,}]+)/gi;
|
|
3
|
+
const AUTH_SCHEME_RE = /\b(Bearer|Basic|ApiKey)\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
4
|
+
function redactPii(text) {
|
|
5
|
+
return text.replace(EMAIL_RE, "[redacted-email]").replace(SECRET_HEADER_RE, (_match, name, separator) => `${name}${separator}[redacted]`).replace(AUTH_SCHEME_RE, (_match, scheme) => `${scheme} [redacted]`);
|
|
6
|
+
}
|
|
7
|
+
const SECRET_KEY_RE = /(authorization|cookie|password|passwd|\bpwd\b|secret|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|auth[-_]?token|^token$|\bbearer\b|private[-_]?key|credential)/i;
|
|
8
|
+
function redactAttributes(attributes) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
11
|
+
if (SECRET_KEY_RE.test(key)) result[key] = "[redacted]";
|
|
12
|
+
else if (typeof value === "string") result[key] = redactPii(value);
|
|
13
|
+
else result[key] = value;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
redactAttributes,
|
|
19
|
+
redactPii
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/redact.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Attributes } from '../types'\n\n/**\n * Active redaction backstop for telemetry (Privacy section of the telemetry spec).\n *\n * Defense-in-depth over the don't-emit posture: callers are expected not to put\n * PII or secrets into errors/logs, but error messages and stack frames can still\n * pick them up a layer down (e.g. `Error: no user for jan.kowalski@example.com`,\n * or a dumped `Authorization: Bearer \u2026` header). This scrubs the highest-signal\n * identifiers \u2014 email addresses and auth tokens \u2014 from any text that ships to the\n * backend, without touching opaque UUIDs/ids (which we keep).\n *\n * Deliberately conservative (emails + auth tokens) to preserve debuggability;\n * extend the pattern set here if a new leak vector is found.\n */\n// Bound every component both to match the practical limits from RFC 5321 and\n// to keep scanning time linear for hostile strings with long runs of email\n// punctuation (CodeQL js/polynomial-redos).\nconst EMAIL_RE = /[A-Z0-9._%+-]{1,64}@[A-Z0-9.-]{1,253}\\.[A-Z]{2,63}/gi\n// `authorization: <value>` / `cookie: <value>` header dumps embedded in free text.\n// The value runs to the next separator (`;`/`,`/`}`) or newline, so it swallows a\n// `Bearer <token>` value in one pass (before the standalone-scheme rule below).\nconst SECRET_HEADER_RE = /\\b(authorization|proxy-authorization|cookie|set-cookie)(\\s*[:=]\\s*)([^\\n;,}]+)/gi\n// `Bearer <token>` / `Basic <base64>` / `ApiKey <key>` standing alone in free text (message or stack).\nconst AUTH_SCHEME_RE = /\\b(Bearer|Basic|ApiKey)\\s+[A-Za-z0-9._~+/=-]+/gi\n\nexport function redactPii(text: string): string {\n return text\n .replace(EMAIL_RE, '[redacted-email]')\n .replace(SECRET_HEADER_RE, (_match, name: string, separator: string) => `${name}${separator}[redacted]`)\n .replace(AUTH_SCHEME_RE, (_match, scheme: string) => `${scheme} [redacted]`)\n}\n\n// Attribute KEYS whose value is a secret regardless of content. Intentionally\n// specific enough not to clobber benign fields like `token_count`, while\n// still masking the exact key `token`.\nconst SECRET_KEY_RE =\n /(authorization|cookie|password|passwd|\\bpwd\\b|secret|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|auth[-_]?token|^token$|\\bbearer\\b|private[-_]?key|credential)/i\n\n/**\n * Redact a telemetry attribute bag before it ships: a value under a secret-looking\n * KEY (`authorization`, `set-cookie`, `client_secret`, `x-api-key`, \u2026) is masked\n * wholesale; other string values still pass through `redactPii` so an inline email\n * or auth token is caught too. Non-string values are left as-is.\n */\nexport function redactAttributes(attributes: Attributes): Attributes {\n const result: Attributes = {}\n for (const [key, value] of Object.entries(attributes)) {\n if (SECRET_KEY_RE.test(key)) result[key] = '[redacted]'\n else if (typeof value === 'string') result[key] = redactPii(value)\n else result[key] = value\n }\n return result\n}\n"],
|
|
5
|
+
"mappings": "AAkBA,MAAM,WAAW;AAIjB,MAAM,mBAAmB;AAEzB,MAAM,iBAAiB;AAEhB,SAAS,UAAU,MAAsB;AAC9C,SAAO,KACJ,QAAQ,UAAU,kBAAkB,EACpC,QAAQ,kBAAkB,CAAC,QAAQ,MAAc,cAAsB,GAAG,IAAI,GAAG,SAAS,YAAY,EACtG,QAAQ,gBAAgB,CAAC,QAAQ,WAAmB,GAAG,MAAM,aAAa;AAC/E;AAKA,MAAM,gBACJ;AAQK,SAAS,iBAAiB,YAAoC;AACnE,QAAM,SAAqB,CAAC;AAC5B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACrD,QAAI,cAAc,KAAK,GAAG,EAAG,QAAO,GAAG,IAAI;AAAA,aAClC,OAAO,UAAU,SAAU,QAAO,GAAG,IAAI,UAAU,KAAK;AAAA,QAC5D,QAAO,GAAG,IAAI;AAAA,EACrB;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { currentSpan } from "./tracer.js";
|
|
3
|
+
import { serializeError } from "./serialize.js";
|
|
4
|
+
import { counter } from "./meter.js";
|
|
5
|
+
import { redactAttributes } from "./redact.js";
|
|
6
|
+
const logger = createLogger("telemetry");
|
|
7
|
+
function reportError(error, ctx) {
|
|
8
|
+
const span = currentSpan();
|
|
9
|
+
if (span) {
|
|
10
|
+
span.recordException(error);
|
|
11
|
+
span.setStatus("error");
|
|
12
|
+
}
|
|
13
|
+
const serialized = serializeError(error);
|
|
14
|
+
const attributes = { ...ctx?.attributes ?? {} };
|
|
15
|
+
if (ctx?.module) attributes.module = ctx.module;
|
|
16
|
+
const safeAttributes = redactAttributes(attributes);
|
|
17
|
+
const safeError = new Error(serialized.message);
|
|
18
|
+
safeError.name = serialized.name;
|
|
19
|
+
safeError.stack = serialized.stack;
|
|
20
|
+
logger.error("Application error reported", { ...safeAttributes, err: safeError });
|
|
21
|
+
counter("om.errors", 1, ctx?.module ? { module: ctx.module } : void 0);
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
reportError
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=report-error.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/report-error.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Attributes } from '../types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { currentSpan } from './tracer'\nimport { serializeError } from './serialize'\nimport { counter } from './meter'\nimport { redactAttributes } from './redact'\n\nconst logger = createLogger('telemetry')\n\nexport type ReportErrorContext = {\n /** Owning module, e.g. 'orders'. Used as the only metric label. */\n module?: string\n /** Low-cardinality, NO-PII attributes (ids ok, never names/content). */\n attributes?: Attributes\n}\n\n/**\n * The error funnel. Additive \u2014 existing `console.error` calls stay valid; this\n * is the path that reaches the active backend. It:\n * 1. records the exception on the active span (errors-as-span-events),\n * 2. emits a structured error log (stack only \u2014 no PII payloads),\n * 3. increments the `om.errors` counter, labeled by `module` only.\n */\nexport function reportError(error: unknown, ctx?: ReportErrorContext): void {\n const span = currentSpan()\n if (span) {\n span.recordException(error)\n span.setStatus('error')\n }\n\n const serialized = serializeError(error)\n const attributes: Attributes = { ...(ctx?.attributes ?? {}) }\n if (ctx?.module) attributes.module = ctx.module\n const safeAttributes = redactAttributes(attributes)\n\n const safeError = new Error(serialized.message)\n safeError.name = serialized.name\n safeError.stack = serialized.stack\n logger.error('Application error reported', { ...safeAttributes, err: safeError })\n counter('om.errors', 1, ctx?.module ? { module: ctx.module } : undefined)\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAEjC,MAAM,SAAS,aAAa,WAAW;AAgBhC,SAAS,YAAY,OAAgB,KAAgC;AAC1E,QAAM,OAAO,YAAY;AACzB,MAAI,MAAM;AACR,SAAK,gBAAgB,KAAK;AAC1B,SAAK,UAAU,OAAO;AAAA,EACxB;AAEA,QAAM,aAAa,eAAe,KAAK;AACvC,QAAM,aAAyB,EAAE,GAAI,KAAK,cAAc,CAAC,EAAG;AAC5D,MAAI,KAAK,OAAQ,YAAW,SAAS,IAAI;AACzC,QAAM,iBAAiB,iBAAiB,UAAU;AAElD,QAAM,YAAY,IAAI,MAAM,WAAW,OAAO;AAC9C,YAAU,OAAO,WAAW;AAC5B,YAAU,QAAQ,WAAW;AAC7B,SAAO,MAAM,8BAA8B,EAAE,GAAG,gBAAgB,KAAK,UAAU,CAAC;AAChF,UAAQ,aAAa,GAAG,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,MAAS;AAC1E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { redactPii } from "./redact.js";
|
|
2
|
+
function serializeError(error) {
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
return {
|
|
5
|
+
name: redactPii(error.name || "Error"),
|
|
6
|
+
message: redactPii(foldCause(error)),
|
|
7
|
+
stack: error.stack ? redactPii(error.stack) : void 0
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return { name: "NonError", message: redactPii(safeString(error)) };
|
|
11
|
+
}
|
|
12
|
+
function foldCause(error, seen = /* @__PURE__ */ new WeakSet()) {
|
|
13
|
+
if (seen.has(error)) return error.message;
|
|
14
|
+
seen.add(error);
|
|
15
|
+
const cause = error.cause;
|
|
16
|
+
if (cause instanceof Error) {
|
|
17
|
+
return `${error.message} \u2014 caused by ${cause.name || "Error"}: ${foldCause(cause, seen)}`;
|
|
18
|
+
}
|
|
19
|
+
return error.message;
|
|
20
|
+
}
|
|
21
|
+
function safeString(value) {
|
|
22
|
+
if (typeof value === "string") return value;
|
|
23
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "symbol") return value.description ? `Symbol(${value.description})` : "Symbol";
|
|
27
|
+
if (typeof value === "function") return "[function]";
|
|
28
|
+
if (value === null) return "null";
|
|
29
|
+
if (value === void 0) return "undefined";
|
|
30
|
+
return "[non-error object]";
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
serializeError
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/serialize.ts"],
|
|
4
|
+
"sourcesContent": ["import type { LogRecord } from '../types'\nimport { redactPii } from './redact'\n\n/**\n * Serialize an error for telemetry: name, message, and stack only.\n *\n * Deliberately does NOT include arbitrary error properties (request bodies,\n * `cause` payloads, query parameters) \u2014 those can carry PII. The cause CHAIN is\n * folded into the message text (names/messages only), never its data. Message +\n * stack are run through `redactPii` so a leaked email never reaches the backend\n * (active-redaction backstop over the don't-emit posture).\n */\nexport function serializeError(error: unknown): NonNullable<LogRecord['error']> {\n if (error instanceof Error) {\n return {\n name: redactPii(error.name || 'Error'),\n message: redactPii(foldCause(error)),\n stack: error.stack ? redactPii(error.stack) : undefined,\n }\n }\n return { name: 'NonError', message: redactPii(safeString(error)) }\n}\n\nfunction foldCause(error: Error, seen = new WeakSet<object>()): string {\n if (seen.has(error)) return error.message\n seen.add(error)\n const cause = (error as { cause?: unknown }).cause\n if (cause instanceof Error) {\n return `${error.message} \u2014 caused by ${cause.name || 'Error'}: ${foldCause(cause, seen)}`\n }\n return error.message\n}\n\nfunction safeString(value: unknown): string {\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value)\n }\n if (typeof value === 'symbol') return value.description ? `Symbol(${value.description})` : 'Symbol'\n if (typeof value === 'function') return '[function]'\n if (value === null) return 'null'\n if (value === undefined) return 'undefined'\n // Never stringify arbitrary objects: request bodies, provider errors, and\n // thrown payloads commonly contain credentials or personal data.\n return '[non-error object]'\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,iBAAiB;AAWnB,SAAS,eAAe,OAAiD;AAC9E,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,MAAM,UAAU,MAAM,QAAQ,OAAO;AAAA,MACrC,SAAS,UAAU,UAAU,KAAK,CAAC;AAAA,MACnC,OAAO,MAAM,QAAQ,UAAU,MAAM,KAAK,IAAI;AAAA,IAChD;AAAA,EACF;AACA,SAAO,EAAE,MAAM,YAAY,SAAS,UAAU,WAAW,KAAK,CAAC,EAAE;AACnE;AAEA,SAAS,UAAU,OAAc,OAAO,oBAAI,QAAgB,GAAW;AACrE,MAAI,KAAK,IAAI,KAAK,EAAG,QAAO,MAAM;AAClC,OAAK,IAAI,KAAK;AACd,QAAM,QAAS,MAA8B;AAC7C,MAAI,iBAAiB,OAAO;AAC1B,WAAO,GAAG,MAAM,OAAO,qBAAgB,MAAM,QAAQ,OAAO,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,EACzF;AACA,SAAO,MAAM;AACf;AAEA,SAAS,WAAW,OAAwB;AAC1C,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,aAAa,OAAO,UAAU,UAAU;AACxF,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,cAAc,UAAU,MAAM,WAAW,MAAM;AAC3F,MAAI,OAAO,UAAU,WAAY,QAAO;AACxC,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,UAAU,OAAW,QAAO;AAGhC,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getActiveProvider } from "../provider/registry.js";
|
|
2
|
+
import { runWithSpan, spanFromStore } from "./context.js";
|
|
3
|
+
function withSpan(name, fn, options) {
|
|
4
|
+
return getActiveProvider().runInSpan(
|
|
5
|
+
name,
|
|
6
|
+
options ?? {},
|
|
7
|
+
(span) => runWithSpan(span, () => fn(span))
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function currentSpan() {
|
|
11
|
+
return spanFromStore() ?? getActiveProvider().activeSpan();
|
|
12
|
+
}
|
|
13
|
+
function setAttributes(attributes) {
|
|
14
|
+
currentSpan()?.setAttributes(attributes);
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
currentSpan,
|
|
18
|
+
setAttributes,
|
|
19
|
+
withSpan
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=tracer.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/facade/tracer.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Attributes, Span, SpanOptions } from '../types'\nimport { getActiveProvider } from '../provider/registry'\nimport { runWithSpan, spanFromStore } from './context'\n\n/**\n * Run `fn` inside a new span named `name`. The active provider owns span\n * creation + context, so any OTEL auto-instrumentation (pg/http) inside `fn`\n * nests under this span in the same trace. Exceptions and duration are recorded\n * automatically by the provider. No-op-cheap when telemetry is off.\n */\nexport function withSpan<T>(name: string, fn: (span: Span) => T, options?: SpanOptions): T {\n return getActiveProvider().runInSpan(name, options ?? {}, (span) =>\n runWithSpan(span, () => fn(span)),\n )\n}\n\n/** The active span (facade-created or OTEL auto-instrumented), if any. */\nexport function currentSpan(): Span | undefined {\n return spanFromStore() ?? getActiveProvider().activeSpan()\n}\n\n/** Set attributes on the active span, if any. */\nexport function setAttributes(attributes: Attributes): void {\n currentSpan()?.setAttributes(attributes)\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAClC,SAAS,aAAa,qBAAqB;AAQpC,SAAS,SAAY,MAAc,IAAuB,SAA0B;AACzF,SAAO,kBAAkB,EAAE;AAAA,IAAU;AAAA,IAAM,WAAW,CAAC;AAAA,IAAG,CAAC,SACzD,YAAY,MAAM,MAAM,GAAG,IAAI,CAAC;AAAA,EAClC;AACF;AAGO,SAAS,cAAgC;AAC9C,SAAO,cAAc,KAAK,kBAAkB,EAAE,WAAW;AAC3D;AAGO,SAAS,cAAc,YAA8B;AAC1D,cAAY,GAAG,cAAc,UAAU;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|