Observability & Operations

Observability

Logs, traces, metrics — understand what's happening

PURISTA has built-in OpenTelemetry support. Every message — command, subscription, stream, or queue job — automatically creates spans, carries trace context, and emits structured logs. You do not instrument your business logic.

What you get for free

Observability signalWhat PURISTA providesWhat you add
TracesSpans for every message route, command, subscription, stream, and queue jobConfigure an exporter
LogsStructured JSON logs with trace IDs, service names, and message metadataNone — use the provided logger
MetricsMessage counts, latency histograms, error ratesConfigure a metrics exporter
Error trackingTyped errors with stack traces in spansNone

How tracing works

sequenceDiagram
    autonumber
    participant C as Client
    participant EB as Event Bridge
    participant S1 as User Service
    participant S2 as Email Service
    participant EXP as Exporter

    C->>EB: send command (traceId: abc123)
    EB->>EB: start span: event_bridge.route
    EB->>S1: deliver command
    S1->>S1: start span: userService.userSignUp
    S1->>S1: log: { event: 'user.created', userId: '...' }
    S1->>EB: emit event (same traceId)
    EB->>S2: deliver event
    S2->>S2: start span: emailService.sendWelcomeEmail
    S2->>EB: done
    EB->>C: return response
    EB->>EXP: flush spans

Each span includes:

  • Trace ID — correlates the entire distributed flow
  • Service name and version — know exactly which service handled the message
  • Command or subscription name — pinpoint the operation
  • Timing — how long each hop took
  • Logs — structured JSON logs attached to the span

Quick setup

PURISTA instruments traces by accepting a SpanProcessor at construction time. Pass it to the event bridge and each service — no changes to commands or subscriptions:

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { DefaultEventBridge } from '@purista/core'

const spanProcessor = new SimpleSpanProcessor(
  new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' })
)

const eventBridge = new DefaultEventBridge({ spanProcessor })
const myService = await myV1Service.getInstance(eventBridge, { spanProcessor })

For additional auto-instrumentation of Node.js built-ins, you can also initialize NodeSDK before starting PURISTA — both approaches are compatible. See OpenTelemetry Backends for all supported exporters and backends.

Supported backends

BackendProtocolSetup
JaegerOTLP HTTPSingle Docker container
Grafana TempoOTLP HTTPWorks with existing Grafana stack
ZipkinZipkin wireSingle Docker container
SigNozOTLP HTTPFull observability platform
AWS X-RayOTLP / ADOT CollectorIAM and ADOT setup
Azure MonitorOTLPAzure resource setup
Google Cloud TraceCloud Trace exporterGCP project setup

All backends use SimpleSpanProcessor with the appropriate exporter — see the OpenTelemetry Backends guide for details on each.

Structured logging

PURISTA uses pino under the hood. Logs include:

  • serviceName and serviceVersion
  • serviceTarget (command or subscription name)
  • principalId and tenantId
  • traceId for correlation
  • OpenTelemetry trace context
context.logger.info({ userId: payload.userId }, 'User created')

Output:

{
  "level": 30,
  "time": 1700000000000,
  "serviceName": "UserService",
  "serviceVersion": "1",
  "serviceTarget": "userSignUp",
  "traceId": "abc123",
  "principalId": "user-456",
  "msg": "User created",
  "userId": "user-789"
}

Business metrics

For business metrics, emit custom events from commands:

.setCommandFunction(async function (context, payload) {
  const result = await processOrder(payload)
  await context.emit('orderCompleted', {
    orderId: result.id,
    amount: result.amount,
    currency: result.currency,
  })
  return result
})

Subscribe with an analytics service:

const analyticsSubscription = analyticsServiceBuilder
  .getSubscriptionBuilder('onOrderCompleted', 'Track orders')
  .subscribeToEvent('orderCompleted')
  .setSubscriptionFunction(async function (context, payload) {
    await context.resources.analytics.track('order.completed', payload)
  })

When to add custom instrumentation

  • Rarely needed — PURISTA covers the message path
  • Add custom spans for complex business logic inside commands
  • Add custom metrics for business KPIs (orders, revenue, active users)

Common pitfalls

  • Not configuring an exporter. Traces are generated but never sent anywhere.
  • Ignoring log levels. Production should use warn or info, not debug.
  • Logging sensitive data. Never log secrets, passwords, or tokens.
  • Missing trace correlation. Ensure all external HTTP calls include trace headers.

Checklist

  • OpenTelemetry exporter is configured and receiving traces
  • Log level is appropriate for the environment
  • Logs do not contain sensitive data
  • Business metrics are emitted as custom events
  • Trace correlation works across service boundaries
  • Alerts are configured for error rates and latency

Related

Read Next
Getting Started

from Learning Paths & Tutorials