Skip to content

Local Stacks

Instrumenting code you cannot see the output of is guesswork. The repo ships compose files so a backend is one command away, with no account to create and nothing to bill you.

Stack Signals Start UI
Jaeger Traces docker compose -f docker/jaeger.yml up -d http://localhost:16686
LGTM Traces, metrics, logs docker compose -f docker/lgtm.yml up -d http://localhost:3000
Langfuse LLM traces, cost, evaluations docker compose -f docker/langfuse.yml up -d http://localhost:3000

Swap up -d for down -v to stop and discard the data.

Pick Jaeger when you only care about traces and want the smallest thing that works. Pick LGTM when you want metrics or logs as well, or when you want to query your telemetry back from autotel-mcp.

Pick Langfuse when the thing you are debugging is an LLM call rather than a request: it reads gen_ai.* spans as generations, prices them, and keeps prompts and responses next to the trace.

All three take OTLP straight from your app. Put a collector in front once you want to sample whole traces, mask attributes autotel does not recognise, or count requests before anything is dropped.

Terminal window
docker compose -f docker/jaeger.yml up -d
Terminal window
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
import { init } from 'autotel';
init({ service: 'checkout-api' });

Traces land at http://localhost:16686.

Grafana’s all-in-one image runs Loki, Grafana, Tempo and Mimir in one container. Anonymous admin is on, so there is no password to look up.

Terminal window
docker compose -f docker/lgtm.yml up -d
Port Service Used for
3000 Grafana The UI
3100 Loki Log push + query
3200 Tempo Trace query
9090 Prometheus Metric query
4317 OTLP gRPC Ingest
4318 OTLP HTTP Ingest

The three query ports are published on purpose. Ingest alone would let you write telemetry you could never read back from a tool.

Terminal window
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
Terminal window
LOKI_ENDPOINT=http://localhost:3100
import { init } from 'autotel';
import { LokiSubscriber } from 'autotel-subscribers/loki';
init({
service: 'checkout-api',
eventSubscribers: [new LokiSubscriber()],
});

Then in Grafana, filter by label and reach into the event with | json:

{service="checkout-api"} | json | durationMs > 1000

See Grafana Loki for label and cardinality guidance.

Langfuse ingests plain OTLP at /api/public/otel, so autotel reaches it through destinations and needs no Langfuse SDK.

Terminal window
docker compose -f docker/langfuse.yml up -d

Six containers, roughly two minutes to first boot. Langfuse needs Postgres, ClickHouse, Redis and MinIO, so there is no single-container version the way LGTM has one. Wait for it rather than guessing:

Terminal window
until curl -sf http://localhost:3000/api/public/health >/dev/null; do sleep 5; done

The project is provisioned on first start from LANGFUSE_INIT_*, so the keys are fixed and you never open the UI to copy one:

Terminal window
LANGFUSE_BASEURL=http://localhost:3000
LANGFUSE_PUBLIC_KEY=pk-lf-0d5c0dc9-3b4f-4f3c-9d3a-000000000001
LANGFUSE_SECRET_KEY=sk-lf-0d5c0dc9-3b4f-4f3c-9d3a-000000000002
import { init } from 'autotel';
const auth = Buffer.from(
`${process.env.LANGFUSE_PUBLIC_KEY}:${process.env.LANGFUSE_SECRET_KEY}`,
).toString('base64');
init({
service: 'support-agent',
destinations: [
{
endpoint: `${process.env.LANGFUSE_BASEURL}/api/public/otel`,
headers: { Authorization: `Basic ${auth}` },
signals: ['traces'],
},
],
});

Langfuse turns a chat span into a generation, reads gen_ai.usage.input_tokens and gen_ai.usage.output_tokens as usage, and prices the call from its own model table. Sign in at http://localhost:3000 as dev@example.com / localdevpassword.

Point the MCP server at the query ports and let it work out what is running:

Terminal window
AUTOTEL_BACKEND=auto \
TEMPO_BASE_URL=http://localhost:3200 \
PROMETHEUS_BASE_URL=http://localhost:9090 \
LOKI_BASE_URL=http://localhost:3100 \
npx autotel-mcp

Autodetection probes /api/echo on Tempo, /api/v1/status/buildinfo on Prometheus and /ready on Loki, then uses whichever answer. An agent can ask about a slow request and get it out of Tempo without you naming a backend.

autotel-devtools listens on 4318 by default, which is the port LGTM binds for OTLP HTTP. To run both, move devtools:

Terminal window
AUTOTEL_DEVTOOLS_PORT=4319 npx autotel-devtools

Send to whichever you want to read from: devtools at http://127.0.0.1:4319 for a live view of the request you just made, LGTM at http://localhost:4318 for history and querying.

With Jaeger there is no clash, because Jaeger only takes the OTLP ports and devtools can keep 4318 if Jaeger is down.

One file per stack under docker/, so a new backend never means editing a shared file. Each should carry:

  • a name: matching the stack, so the compose project is stable wherever it is run from
  • a container_name, so docker logs is predictable
  • a healthcheck gating on the component that starts last, not on the UI — a healthy Grafana does not mean Loki is accepting writes
  • a comment naming any port that clashes with an existing stack

See docker/README.md in the repo.