A self-contained TypeScript example showing how to send GenAI telemetry to Galileo through an OpenTelemetry Collector, with a local Jaeger UI for visual span inspection.
[!NOTE]
+------------------+ +-------------------+ +--------------------+
| TypeScript App | OTLP | OTEL Collector | OTLP | Galileo |
| (Node.js, host) |------>| (Docker) |------>| otel/v1/traces |
| | HTTP | | HTTP | |
+------------------+ :4318 +------|------------+ +--------------------+
|
| OTLP/gRPC
v
+-------------------+
| Jaeger UI |
| (Docker) |
| localhost:16686 |
+-------------------+
Data flow:
- The TypeScript app creates OTEL spans with GenAI semantic attributes
- Spans are exported via OTLP/HTTP to the local OTEL Collector (port 4318)
- The collector fans out to two destinations:
- Galileo via OTLP/HTTP with Galileo auth headers
- Jaeger via OTLP/gRPC for local visualization
The app sends no Galileo-specific headers — the collector owns the export config, which mirrors production deployments where the collector is a centralized gateway.
Each conversation turn is its own OTEL trace. Galileo groups them into a
single session because they share the same session.id attribute.
Session (Galileo groups traces by session.id)
│
├── Trace 1 ── invoke_agent "restaurant_assistant"
│ ├── chat (LLM: decides to use web_search)
│ ├── execute_tool (web_search: returns restaurant list)
│ └── chat (LLM: synthesizes recommendation)
│
└── Trace 2 ── invoke_agent "restaurant_assistant"
├── chat (LLM: decides to check availability)
├── execute_tool (check_availability: returns time slots)
└── chat (LLM: presents reservation options)
| Span Type | Key Attributes |
|---|---|
Agent root (invoke_agent) |
session.id, gen_ai.operation.name, gen_ai.agent.name |
LLM (chat) |
gen_ai.operation.name, gen_ai.request.model, gen_ai.provider.name, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.response.finish_reasons |
Tool (execute_tool) |
gen_ai.operation.name, gen_ai.tool.name, gen_ai.tool.call.id, gen_ai.tool.call.arguments, gen_ai.tool.call.result |
Agent and LLM spans carry input/output via OTEL events: gen_ai.user.message and gen_ai.choice.
- Node.js 18+
- Docker and Docker Compose
- A Galileo account with API key
# 1. Configure
cp .env.example .env
# Edit .env with your Galileo credentials
# 2. Install dependencies
npm install
# 3. Start infrastructure (Collector + Jaeger)
npm run infra:up
# 4. Run the demo
npm start # Simulated mode (no OpenAI key needed)
# OR
npm run start:real # Real OpenAI mode (requires OPENAI_API_KEY in .env)
# 5. Tear down
npm run infra:down| Script | Command | What it does |
|---|---|---|
npm start |
npx tsx src/index.ts |
Runs the demo in simulated mode (mock LLM responses, no API keys needed) |
npm run start:real |
USE_REAL_LLM=true npx tsx src/index.ts |
Runs the demo with real OpenAI API calls (requires OPENAI_API_KEY) |
npm run infra:up |
docker compose up -d |
Starts the OTEL Collector and Jaeger containers in the background |
npm run infra:down |
docker compose down |
Stops and removes the containers |
npm run build |
tsc |
Compiles TypeScript to dist/ (not required to run — tsx handles it) |
npm run infra:up starts two containers defined in docker-compose.yml:
-
Image:
otel/opentelemetry-collector-contrib:0.115.0 -
Role: Receives OTLP traces from the TypeScript app and forwards them to Galileo and Jaeger. This is the central routing layer — the app itself has no knowledge of Galileo credentials.
-
Config: Reads
otel-collector-config.yaml(mounted as a volume). The config defines one receiver (OTLP on ports 4317/4318), a batch processor, and three exporters (Galileo, Jaeger, debug console). -
Exposed ports:
Port Protocol Purpose 4317gRPC OTLP gRPC receiver (available but not used by this demo) 4318HTTP OTLP HTTP receiver — the app sends traces here 8888HTTP Collector's own metrics (Prometheus format) -
Environment variables:
GALILEO_CONSOLE_URL,GALILEO_API_KEY,GALILEO_PROJECT,GALILEO_LOG_STREAM— all read from your.envfile via docker-compose. The collector config references these with${VAR}syntax to set the Galileo export endpoint and auth headers. -
Logs:
docker compose logs -f otel-collector— thedebugexporter prints a summary line for every span batch, useful for confirming traces are flowing.
-
Image:
jaegertracing/all-in-one:1.62 -
Role: Receives traces from the collector and provides a web UI for inspecting span trees, attributes, and timing. Runs an all-in-one deployment (collector + query + UI in a single container) with in-memory storage.
-
Exposed ports:
Port Protocol Purpose 16686HTTP Jaeger UI — open http://localhost:16686 in your browser 4317(internal)gRPC OTLP gRPC receiver — only the collector connects here (not mapped to host) -
No configuration needed: Jaeger's OTLP collector is enabled via the
COLLECTOR_OTLP_ENABLED=trueenvironment variable. Storage is in-memory — traces are lost when the container stops, which is fine for a demo.
# Check container status
docker compose ps
# Expected output:
# NAME IMAGE STATUS
# jaeger jaegertracing/all-in-one:1.62 Up
# otel-collector otel/opentelemetry-collector-contrib:0.115.0 Up
# Confirm the collector is healthy and listening
curl -s http://localhost:8888/metrics | head -5
# Confirm Jaeger UI is accessible
curl -s -o /dev/null -w "%{http_code}" http://localhost:16686
# Should print: 200In Jaeger (http://localhost:16686)
- Open http://localhost:16686 in your browser
- In the Service dropdown, select otel-ts-collector-demo
- Click Find Traces — you should see two traces (one per conversation turn)
- Click on a trace to expand the span tree:
invoke_agent— root span (carriessession.idand agent name)chat— LLM call (decides which tool to use)execute_tool— tool call (web_search or check_availability)chat— LLM call (synthesizes final answer)
- Click any span to inspect its Tags (attributes) and Logs (events):
- Agent spans show
gen_ai.agent.nameand input/output events - LLM spans show
gen_ai.request.model, token counts, andgen_ai.choiceevents - Tool spans show
gen_ai.tool.name, arguments, and result
- Agent spans show
- Navigate to your project (the
GALILEO_PROJECTvalue from.env) - Select the log stream (
GALILEO_LOG_STREAMfrom.env) - View traces with session grouping — both turns grouped under one session
- Inspect LLM inputs/outputs, token usage, and tool calls
- Check the span hierarchy matches the tree above
The OTEL Collector is configured with Galileo credentials in otel-collector-config.yaml:
exporters:
otlphttp/galileo:
traces_endpoint: "${GALILEO_CONSOLE_URL}/api/galileo/otel/v1/traces"
headers:
Galileo-API-Key: "${GALILEO_API_KEY}"
project: "${GALILEO_PROJECT}"
logstream: "${GALILEO_LOG_STREAM}"These env vars flow from your .env → docker-compose.yml (environment section) → collector container → otel-collector-config.yaml (variable substitution). The app never touches them.
| Problem | Solution |
|---|---|
| No spans in Jaeger | Check collector logs: docker compose logs otel-collector |
| No spans in Galileo | Verify API key and console URL in .env, then check collector logs for HTTP errors |
| Collector won't start | Check config syntax: docker compose logs otel-collector will show parse errors |
| Port 4318 in use | Stop other OTEL collectors or change the port mapping in docker-compose.yml |
| Jaeger UI not loading | Verify the container is running: docker compose ps |
ECONNREFUSED :4318 |
Infrastructure isn't running — run npm run infra:up first |
For verbose OTEL SDK logging, change DiagLogLevel.INFO to DiagLogLevel.DEBUG in src/tracing.ts.