Skip to content

Observability

protoLabs Studio uses a single unified OTel NodeSDK with two span processors:

ProcessorWhat it captures
BatchSpanProcessorExpress HTTP spans, pg queries, fs operations — all standard server traffic
LangfuseSpanProcessorAI SDK calls (streamText, generateText) via experimental_telemetry

Both processors live in one NodeSDK instance (src/lib/otel.ts) and share the same Langfuse credentials. Agent execution is additionally traced via TracedProvider (Langfuse SDK) and Claude subprocess telemetry (CLAUDE_CODE_ENABLE_TELEMETRY).

For self-hosted local observability, see Local OTel Stack below.

Configuration

Set these environment variables to enable tracing:

VariableRequiredDefaultDescription
LANGFUSE_PUBLIC_KEYYesLangfuse project public key. Both integrations are disabled if this is absent.
LANGFUSE_SECRET_KEYYesLangfuse project secret key.
LANGFUSE_BASE_URLNohttps://cloud.langfuse.comOverride for self-hosted Langfuse.
OTEL_SERVICE_NAMENoprotolabs-serverService name that appears in Langfuse trace metadata.

The staging docker-compose.staging.yml maps all four from the host environment with safe defaults:

yaml
- LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY:-}
- LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY:-}
- LANGFUSE_BASE_URL=${LANGFUSE_BASE_URL:-}
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-protolabs-server}

Unified OTel SDK (otel.ts)

Called as the first operation in runStartup(). Configures a single NodeSDK with:

  • BatchSpanProcessor + OTLPTraceExporter pointing to ${LANGFUSE_BASE_URL}/api/public/otel/v1/traces (full URL required — OTLPTraceExporter only auto-appends /v1/traces when using OTEL_EXPORTER_OTLP_ENDPOINT env var, not the programmatic url option)
  • LangfuseSpanProcessor from @langfuse/otel — captures AI SDK experimental_telemetry spans with enriched LLM metadata (model, tokens, cost)
  • Auth: HTTP Authorization: Basic <base64(publicKey:secretKey)> header
  • Instrumentation: getNodeAutoInstrumentations() — covers Express HTTP, pg, fs, and other common Node.js modules

Only one NodeSDK can register the global TracerProvider per process. Using two separate NodeSDK instances causes the second to silently no-op. Both processors are registered in a single SDK to ensure both are active.

No-op behavior

If LANGFUSE_PUBLIC_KEY or LANGFUSE_SECRET_KEY are missing, initOtel() logs a WARN-level message and returns without registering the SDK. No error is thrown.

Agent Tracing (Three Layers)

LayerWhatHow
TracedProviderTop-level agent run (latency, tokens, tool spans, cost)ProviderFactory.wrapWithTracing() wraps provider with Langfuse SDK
LangfuseSpanProcessorAI SDK calls (streamText, generateText) in /api/chat, /api/ai/*experimental_telemetry: { isEnabled: true } on each call
Claude subprocess OTelPer-turn token usage, API request costs, tool result timingsCLAUDE_CODE_ENABLE_TELEMETRY=1 passed in subprocess env

The Claude Agent SDK runs as a claude -p subprocess. The parent process traces the message stream via TracedProvider. The subprocess emits its own OTel spans (if Langfuse credentials are configured) as separate traces, correlated by featureId resource attribute.

AI SDK telemetry example

typescript
const result = await streamText({
  model: anthropic('claude-opus-4-6'),
  prompt: 'Hello',
  experimental_telemetry: { isEnabled: true, functionId: 'my-function' },
});

Viewing Traces

  1. Open Langfuse (or your self-hosted instance)
  2. Navigate to Traces
  3. Filter by Service Name = protolabs-server to isolate server traffic
  4. Click any trace to inspect spans, durations, and attributes

All spans land in the same Langfuse project — OTLP HTTP spans and AI SDK spans share credentials and appear together in the traces list.

Graceful Shutdown

The OTel SDK and Langfuse client are flushed during graceful shutdown (shutdown.ts):

SIGTERM / SIGINT
  -> gracefulShutdown()
    -> shutdownLangfuse()   // flushes Langfuse SDK client queue (TracedProvider traces)
    -> shutdownOtel()       // flushes both span processors (OTLP batch + Langfuse)
    -> server.close()

The shutdown sequence waits for all pending spans to be exported before closing the HTTP server.

Adding Instrumentation to a New Service

Auto-instrumentation covers most standard Node.js I/O automatically. For custom business logic spans, use the OTel API directly:

typescript
import { trace } from '@opentelemetry/api';

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

async function doWork() {
  return tracer.startActiveSpan('my-operation', async (span) => {
    span.setAttribute('feature.id', featureId);
    try {
      const result = await someOperation();
      return result;
    } catch (err) {
      span.recordException(err as Error);
      throw err;
    } finally {
      span.end();
    }
  });
}

Local OTel Stack (Grafana Alloy)

For local development and self-hosted deployments, protoLabs Studio provides a complete OTel pipeline via docker-compose.observability.yml.

Architecture

protoLabs Server
  |
  | OTLP gRPC :4317 / HTTP :4318
  v
+------------------+
|  Grafana Alloy   |  (OTel Collector)
+------------------+
  |         |         |
  | traces  | metrics | logs
  v         v         v
+-------+ +-------+ +-------+
| Tempo | | Mimir | |  Loki |
+-------+ +-------+ +-------+
  |         |         |
  +---------+---------+
            |
            v
       +----------+
       |  Grafana  |  :3011
       +----------+

Services:

ServiceImagePortPurpose
Grafana Alloygrafana/alloy:latest4317, 4318, 12345OTel Collector — receives OTLP, routes to backends
Tempografana/tempo:latest3200Distributed trace storage
Lokigrafana/loki:3.2.03100Log aggregation
Mimirgrafana/mimir:latest9009Long-term metrics storage
Grafanagrafana/grafana:latest3011Dashboards and visualization

Quick Start

bash
# Start all five services
docker compose -f docker-compose.observability.yml up -d

# Verify all services are healthy
docker compose -f docker-compose.observability.yml ps

# Open Grafana (admin / admin)
open http://localhost:3011

# Tail Alloy logs to confirm data is flowing
docker logs -f automaker-alloy

Pointing the Server at Alloy

Set these environment variables before starting the protoLabs server:

bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_SERVICE_NAME=protolabs-server

Alloy accepts both gRPC (:4317) and HTTP (:4318). The server's otel.ts uses HTTP by default; either endpoint works.

Grafana Datasources

Three datasources are pre-provisioned at startup (no manual configuration required):

NameUIDURLDefault
Mimirmimirhttp://mimir:9009/prometheusYes
Tempotempohttp://tempo:3200No
Lokilokihttp://loki:3100No

Tempo is linked to Mimir for service graph metrics, and to Loki for log correlation via trace_id.

Dashboard: protoLabs Studio — Automation Pipeline

The pre-provisioned dashboard (observability/grafana/dashboards/protolabs.json) provides:

PanelMetric sourceDescription
Automation Run Counttraces_spanmetrics_calls_totalTotal automation/feature span invocations
p50 Durationtraces_spanmetrics_latency_bucket (p50)Median automation duration in ms
p95 Durationtraces_spanmetrics_latency_bucket (p95)95th percentile automation duration in ms
Overall Error Ratetraces_spanmetrics_calls_total (error ratio)Fraction of spans with error status
Automation Run Rate by IDtraces_spanmetrics_calls_total by automation_idPer-automation throughput over time
Error Rate by automationIderror ratio by automation_idPer-automation error rate over time
p50/p95 Duration by IDlatency histograms by automation_idPer-automation latency percentiles
Active Flows Heatmaptraces_spanmetrics_calls_total by span_nameHeatmap of concurrent flow activity

Metrics are generated by Tempo's metrics_generator (span metrics and service graph processors) and written to Mimir via remote write.

Configuration Files

FilePurpose
observability/alloy-config.alloyAlloy River config — OTLP receivers, batch processors, exporters
observability/grafana/datasources.ymlGrafana datasource provisioning (Tempo, Loki, Mimir)
observability/grafana/dashboards/dashboards.ymlGrafana dashboard provider config
observability/grafana/dashboards/protolabs.jsonAutomation pipeline dashboard definition

Stopping the Stack

bash
docker compose -f docker-compose.observability.yml down

# Remove all persistent volumes (clears all stored data)
docker compose -f docker-compose.observability.yml down -v

Built by protoLabs — Open source on GitHub