A distributed tracing setup for microservices. You run three small services that call each other, capture latency and errors with OpenTelemetry, store everything in PostgreSQL, and browse it through a React dashboard. Works locally with Docker Compose or on a Kubernetes cluster with the manifests in k8s/.
Built as a portfolio and learning project. Not production hardened, but the full path from instrumented service to UI is there.
Three demo services simulate a simple order flow. When you hit the order endpoint, the request chains through payment and inventory. Each hop creates spans with timing and status. The API rolls that into latency percentiles, error counts, and a basic service graph. The UI shows traces as a waterfall, lists failed spans, and lets you filter by service.
flowchart LR
subgraph clients [Clients]
User[Browser / curl]
end
subgraph services [Demo Microservices]
Order[order-service\n:3001]
Payment[payment-service\n:3002]
Inventory[inventory-service\n:3003]
end
subgraph platform [Observability]
Collector[OTel Collector\n:4317]
API[trace-api\n:4000]
DB[(PostgreSQL\n:5432)]
UI[trace-ui\n:5173]
end
User -->|POST /orders| Order
Order -->|POST /process| Payment
Payment -->|POST /check| Inventory
Order -->|spans| API
Payment -->|spans| API
Inventory -->|spans| API
Order -.->|K8s mode| Collector
Payment -.->|K8s mode| Collector
Inventory -.->|K8s mode| Collector
Collector -.->|OTLP JSON| API
API --> DB
User -->|HTTP| UI
UI -->|REST| API
Locally, demo services send spans straight to trace-api over HTTP. On Kubernetes they go through the OpenTelemetry Collector first, which batches and forwards to the API.
sequenceDiagram
participant Client
participant Order as order-service
participant Payment as payment-service
participant Inventory as inventory-service
participant API as trace-api
participant DB as PostgreSQL
participant UI as trace-ui
Client->>Order: POST /orders
Order->>Payment: POST /process
Payment->>Inventory: POST /check
Inventory-->>Payment: stock OK
Payment-->>Order: payment OK
Order-->>Client: order confirmed
Note over Order,Inventory: Each service exports spans as the request moves
Order->>API: span batch
Payment->>API: span batch
Inventory->>API: span batch
API->>DB: store spans
UI->>API: GET /v1/traces
API->>DB: query
DB-->>API: results
API-->>UI: trace list + metrics
Everything runs in the kube-trace namespace.
flowchart TB
subgraph ns [Namespace kube-trace]
Ingress[Ingress\nkube-trace.local]
subgraph apps [Applications]
UI_Pod[trace-ui]
API_Pod[trace-api]
Order_Pod[order-service]
Pay_Pod[payment-service]
Inv_Pod[inventory-service]
end
subgraph infra [Infrastructure]
PG[(postgres)]
OTel[otel-collector]
end
Ingress --> UI_Pod
Ingress --> API_Pod
Order_Pod --> Pay_Pod
Pay_Pod --> Inv_Pod
Order_Pod --> OTel
Pay_Pod --> OTel
Inv_Pod --> OTel
OTel --> API_Pod
API_Pod --> PG
UI_Pod --> API_Pod
end
| Layer | Tools |
|---|---|
| Backend API | TypeScript, Fastify, PostgreSQL |
| Frontend | React, Vite, TanStack Query, Recharts, Tailwind |
| Tracing | OpenTelemetry SDK, OTel Collector |
| Containers | Docker, multi-stage Dockerfiles |
| Orchestration | Kubernetes manifests (plain YAML, no Helm) |
KubePro/
├── apps/
│ ├── trace-api/ Ingestion and query API
│ ├── trace-ui/ React dashboard
│ └── demo-services/ order, payment, inventory
├── packages/
│ ├── shared/ Shared TypeScript types
│ └── otel-config/ OTel setup and trace-api exporter
├── k8s/ Kubernetes manifests
├── scripts/ Deploy, seed traffic, integration test
├── docker-compose.yml Postgres + optional collector stack
└── otel-collector-config.yaml
You need Node 20+, Docker, and npm.
Install and build shared packages
npm install
npm run build -w @kube-trace/shared
npm run build -w @kube-trace/otel-configStart Postgres
docker compose up -d postgresStart the API (keep this terminal open)
npm run dev:apiStart the three demo services (one terminal each)
npm run dev:inventory
npm run dev:payment
npm run dev:orderStart the UI
npm run dev:uiOpen http://localhost:5173.
Generate some traffic
PowerShell:
.\scripts\seed-traffic.ps1 -Count 20Bash:
./scripts/seed-traffic.sh 20Or send a single order by hand:
curl -X POST http://localhost:3001/orders \
-H "Content-Type: application/json" \
-d '{"item":"book","amount":29.99}'Run the integration test
npm run test:integrationThe OTel Collector is optional for local dev. Services talk to trace-api directly. If you want the collector in the loop:
docker compose up -d otel-collectorThen set OTEL_USE_COLLECTOR=true on the demo services before starting them.
You need kubectl, Docker, and ideally kind for a local cluster.
kind create cluster --name kube-trace
./scripts/deploy-local.shPort-forward the UI and order service:
kubectl -n kube-trace port-forward svc/trace-ui 8080:80
kubectl -n kube-trace port-forward svc/order-service 3001:3001UI at http://localhost:8080. Send orders to port 3001 the same way as local dev.
The deploy script builds images, loads them into kind if available, and applies everything under k8s/.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/spans |
Ingest a batch of spans as JSON |
| POST | /v1/traces |
Ingest OTLP JSON from the collector |
| GET | /v1/traces |
Search traces by service, status, time range |
| GET | /v1/traces/:traceId |
Full trace with all spans |
| GET | /v1/services |
Per-service latency (p50, p95, p99) and error rate |
| GET | /v1/services/:name/errors |
Errors for one service |
| GET | /v1/errors |
Recent errors across all services |
| GET | /v1/dependencies |
Service-to-service call counts |
| GET | /health |
Liveness check |
| GET | /ready |
Readiness check (includes DB) |
The UI has five sections.
Services shows request volume, error rate, and p50/p95/p99 latency per service with a bar chart comparison.
Traces is a searchable list. Filter by service or errors only, then click a trace ID to open the detail view.
Trace detail renders a waterfall timeline. Each row is a span with service name, operation, duration bar, and status.
Errors lists failed spans with the error message and a link back to the full trace.
Service graph draws the order → payment → inventory chain with call counts pulled from parent-child span relationships.
Each demo service initializes OpenTelemetry on boot with auto-instrumentation for HTTP and Fastify. Manual spans wrap the business logic (create-order, process-payment, check-inventory). Random delays and a small error rate keep the demo data interesting.
For local development, spans export as JSON to trace-api through a custom exporter in packages/otel-config. In Kubernetes, OTEL_USE_COLLECTOR=true sends spans to the collector on gRPC port 4317, which forwards OTLP JSON to the API.
The API writes spans into a single spans table. Queries aggregate latency percentiles and build the dependency graph from parent-child links between spans in the same trace.
Each service records its own spans. Cross-service correlation on a single trace ID depends on W3C trace context propagation through the HTTP calls. The demo covers the basics; tighter end-to-end waterfall stitching across all three services is an area you can extend.
| Script | Purpose |
|---|---|
scripts/seed-traffic.sh |
Fire random orders at the order service (Bash) |
scripts/seed-traffic.ps1 |
Same thing for PowerShell |
scripts/deploy-local.sh |
Build images and apply K8s manifests |
scripts/integration-test.mjs |
POST an order and verify spans land in the API |
MIT