See the telemetry spec.
Usage
The canonical application logger remains @open-mercato/shared/lib/logger. Telemetry extends it after successful initialization with trace correlation and one remote sink; it does not create a second logger or local output path.
import { createLogger } from '@open-mercato/shared/lib/logger'
import { withSpan, counter, reportError } from '@open-mercato/telemetry'
const log = createLogger('orders').child({ module: 'sales' })
log.info('Order placed', { orderId })
await withSpan('orders.checkout', async (span) => {
span.setAttribute('om.tenant_id', tenantId)
// pg/undici auto-spans nest here when OTLP is enabled
})
counter('om.errors', 1, { module: 'orders' })
try {
// ...
} catch (error) {
reportError(error, { module: 'orders' })
throw error
}
Use OM_LOG_LEVEL, OM_LOG_PRETTY, and OM_LOG_DESTINATION for both the normal local logger and telemetry-backed log export. Remote records follow the same shared level gate as local records.
Enabling
Leave TELEMETRY_BACKEND unset (or set it to noop) to keep telemetry off. Unknown values also resolve to off.
TELEMETRY_BACKEND=otlp
TELEMETRY_SAMPLING_RATIO=0.1
OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-collector>:4318
OTEL_EXPORTER_OTLP_HEADERS=<auth-header>=<key>
OTEL_SERVICE_NAME=open-mercato
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
otlp, signoz, and newrelic select the same OTLP provider and differ only by endpoint and headers. console is an explicit local span/metric diagnostic backend.
TELEMETRY_TRUST_INBOUND_TRACE defaults to false. In that mode both traceparent and x-original-traceparent from inbound/global carriers are ignored because either header is caller-controlled. Set it to true only behind a trusted upstream when global W3C continuation is required. The queue package's dedicated metadata._trace carrier does not require this flag. The richer bullmq-otel queue spans do use the process-global propagator, so they stay disabled while this flag is false; async queues then use the dedicated carrier.
Custom providers remain supported through registerProvider(). Because the default host guard deliberately imports telemetry only for built-in backend names, a custom-provider bootstrap must import @open-mercato/telemetry, register a provider whose name matches TELEMETRY_BACKEND, and call initTelemetry() directly. Unregistered names remain a hard no-op.
Browser RUM (client-side telemetry)
Server traces can only prove where the time isn't: a page can render an API answer in 50 ms yet leave the user waiting seconds on bundle download, hydration, or a click handler — all invisible to server spans. The @open-mercato/telemetry/browser entry closes that gap with the OpenTelemetry web SDK: document-load and fetch spans from the backoffice.
Interaction spans are deliberately not collected. The upstream UserInteractionInstrumentation patches addEventListener when it starts, but React attaches a single delegated click listener to the root container during hydration — before this SDK boots from useEffect — so the patch would never see a backoffice click. Booting earlier would put the SDK back on the critical path,