OpenTelemetry

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.

How tracing works in PURISTA

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

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

CloudGrid is a PURISTA-powered observability platform for teams that want traces, logs, metrics, dashboards, alerts, and AI evaluation in one product surface. It accepts OpenTelemetry data and keeps AI Harness evaluation evidence close to the production traces that produced it.

For AI evaluation and optimization workflows, see CloudGrid AI Evaluation. Use the Harness eval helpers as the inner loop, then let CloudGrid manage datasets, runs, per-item results, comparisons, and promotion evidence.

Supported backends

BackendPackageSetup complexity
CloudGridOTLP HTTP / gRPCMedium — observability plus AI evaluation
Jaeger@opentelemetry/exporter-trace-otlp-httpLow — single Docker container
Grafana Tempo@opentelemetry/exporter-trace-otlp-httpLow — works with existing Grafana stack
Zipkin@opentelemetry/exporter-zipkinLow — single Docker container
SigNoz@opentelemetry/exporter-trace-otlp-httpMedium — full observability platform
Uptrace@opentelemetry/exporter-trace-otlp-httpMedium — hosted or self-hosted
Teletrace@opentelemetry/exporter-trace-otlp-httpLow — lightweight trace viewer
AWS X-Ray@opentelemetry/exporter-trace-otlp-httpMedium — IAM and service setup
Azure Monitor@azure/monitor-opentelemetry-exporterMedium — Azure resource setup
Google Cloud Trace@google-cloud/opentelemetry-cloud-trace-exporterMedium — GCP project setup

How to wire OpenTelemetry in PURISTA

PURISTA does not use NodeSDK or a global tracer registration. Instead, you create a SpanProcessor (wrapping your exporter) and pass it directly to the event bridge and each service instance. This keeps instrumentation explicit, testable, and entirely under your control.

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
import { metrics } from '@opentelemetry/api'
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { AmqpBridge } from '@purista/amqpbridge'

// 1. Create a SpanProcessor wrapping your exporter
const spanProcessor = new SimpleSpanProcessor(
  new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' })
)

// 2. Set up metrics (optional but recommended)
const metricReader = new PeriodicExportingMetricReader({
  exporter: new OTLPMetricExporter({ url: 'http://localhost:4318/v1/metrics' }),
  exportIntervalMillis: 5000,
})
const meterProvider = new MeterProvider({ readers: [metricReader] })
metrics.setGlobalMeterProvider(meterProvider)
const meter = meterProvider.getMeter('my-app')

// 3. Pass spanProcessor and meter to the event bridge
const eventBridge = new AmqpBridge({ spanProcessor, metrics: { meter } })
await eventBridge.start()

// 4. Pass the same spanProcessor and meter to each service
const myService = await myV1Service.getInstance(eventBridge, {
  spanProcessor,
  metrics: { meter },
})
await myService.start()

Every command, subscription, stream, and queue job inside myService automatically emits spans correlated to the same trace — no changes to your business logic required.

Graceful shutdown

Always shut down both the span processor and meter provider before the process exits:

gracefulShutdown(logger, [
  eventBridge,
  myService,
  {
    name: 'OTSpanProcessor',
    destroy: () => spanProcessor.shutdown(),
  },
  {
    name: 'OTelMeterProvider',
    destroy: () => meterProvider.shutdown(),
  },
])

Business vs. technical metrics

TypeExamplesHow to collect
TechnicalResponse time, error rate, throughput, resource usageOpenTelemetry metrics + exporter
BusinessDaily active users, order volume, conversion rateEmit custom events from commands, aggregate in analytics

For business metrics, emit custom events from your commands and subscribe to them with an analytics service:

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

Next steps