level 12 / opentelemetry

OpenTelemetry for Test Engineers

Understand OpenTelemetry's role in test observability — instrumenting apps and asserting telemetry in tests.

What Is OpenTelemetry?

OpenTelemetry (OTel) is the CNCF standard for generating, collecting, and exporting telemetry data (traces, metrics, logs). It replaces vendor-specific SDKs with a single, portable API.

Key components:

  • API — language-specific interfaces for instrumentation
  • SDK — implementation of the API (sampling, batching, export)
  • Collector — receives, processes, and exports telemetry to backends
  • OTLP — OpenTelemetry Protocol (the wire format)

OTel Architecture

Application                   OTel Collector          Backends
──────────                    ──────────────          ────────
Node.js app                   ┌─────────────┐
├── OTel SDK ──OTLP──────────►│  Receiver   │
│   ├── Traces                │  Processor  ├──► Jaeger (traces)
│   ├── Metrics               │  Exporter   ├──► Prometheus (metrics)
│   └── Logs                  └─────────────┘    ├──► Grafana Loki (logs)

Instrumenting Node.js

// tracing.ts — init before app code
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';

const sdk = new NodeSDK({
  serviceName: 'checkout-api',
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
  }),
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new PgInstrumentation(),
  ],
});

sdk.start();
# Start app with OTel
node -r ./tracing.js app.js

Custom Spans

import { trace, context, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('checkout-service');

async function processPayment(orderId: string, amount: number) {
  return tracer.startActiveSpan('process-payment', async (span) => {
    span.setAttributes({
      'order.id': orderId,
      'payment.amount': amount,
      'payment.currency': 'USD',
    });

    try {
      const result = await chargeCard(amount);
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (err: unknown) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
      throw err;
    } finally {
      span.end();
    }
  });
}

Asserting Telemetry in Tests

Use an in-memory exporter or a test-mode OTel Collector to assert spans in tests:

// test-tracing.ts — in-memory exporter for tests
import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';

export const exporter = new InMemorySpanExporter();
export const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
import { exporter } from './test-tracing';
import { test, expect } from '@playwright/test';

test.beforeEach(() => exporter.reset());

test('payment failure creates error span', async ({ request }) => {
  await request.post('/api/checkout', {
    data: { cardNumber: '4000000000000002', amount: 100 },
  });

  const spans = exporter.getFinishedSpans();
  const paymentSpan = spans.find(s => s.name === 'process-payment');

  expect(paymentSpan).toBeDefined();
  expect(paymentSpan!.status.code).toBe(SpanStatusCode.ERROR);
  expect(paymentSpan!.attributes['order.id']).toBeDefined();
  expect(paymentSpan!.events.some(e => e.name === 'exception')).toBe(true);
});

OTel Collector Config

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 1s

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  prometheus:
    endpoint: "0.0.0.0:8889"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

OTel for Test Automation Insights

// Instrument Playwright itself — trace test execution
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('playwright-tests');

test('instrumented test with custom span', async ({ page }) => {
  const span = tracer.startSpan('e2e-checkout-flow');

  try {
    await page.goto('/checkout');
    await page.fill('[name=card]', '4242424242424242');
    await page.click('[type=submit]');
    await expect(page.getByText('Order confirmed')).toBeVisible();
    span.setStatus({ code: SpanStatusCode.OK });
  } catch (err) {
    span.recordException(err as Error);
    span.setStatus({ code: SpanStatusCode.ERROR });
    throw err;
  } finally {
    span.end();
  }
});