Local LGTM stack (dev / test / CI)
A local LGTM stack in one container
You don't need Grafana Cloud or a collector to see this package working.
grafana/otel-lgtm is an all-in-one image: one OTLP endpoint on 4318
fanning out to Tempo (traces), Loki (logs) and Prometheus
(Metrics), with a pre-provisioned Grafana on
http://localhost:3000. This is exactly what the package is developed
against.
Fastest path — one command
docker run --rm --name lgtm \
-p 3000:3000 -p 4318:4318 -p 9090:9090 -p 3200:3200 -p 3100:3100 \
grafana/otel-lgtm:latest
Wait for The OpenTelemetry collector and the Grafana LGTM stack are up and running in the logs, then point your app at it:
TELEMETRY_ENABLED=true
TELEMETRY_STORE=redis # or "apcu" / "array" for a quick local run
TELEMETRY_EXPORTERS=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# See detail spans (queries, cache, views) on every request while developing:
TELEMETRY_TRACE_DETAILS=always
Hit any route, then open Grafana → Explore → Tempo → Search. Your request is there — click it for the waterfall. That's the whole loop, no collector, no signup.
Persistent stack — docker-compose
For day-to-day dev, a compose file survives restarts and keeps your dashboards and data:
# docker-compose.yml
services:
lgtm:
image: grafana/otel-lgtm:latest
container_name: lgtm
ports:
- "3000:3000" # Grafana UI
- "4318:4318" # OTLP HTTP — the package posts here
- "9090:9090" # Prometheus query API
- "3200:3200" # Tempo query API
- "3100:3100" # Loki query API
volumes:
# Everything persists under /data — Prometheus, Tempo, Loki AND
# Grafana's own db (dashboards, datasources). One volume is enough.
- lgtm-data:/data
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
lgtm-data:
docker compose up -d
docker compose logs -f lgtm # wait for "up and running"
Load the bundled dashboards
The package ships the full Nightwatch-style dashboard suite. Push it into your local Grafana with one command:
php artisan telemetry:dashboards --grafana=http://admin:admin@localhost:3000
They appear under the Telemetry dropdown (a tab bar linking all 13 dashboards). Metrics need scraping or the OTLP flush — for a local run the OTLP push is simplest:
php artisan telemetry:flush # once, or on a schedule / --daemon
Grafana v13 anonymous-mode caveat. Some
grafana/otel-lgtmbuilds ship Grafana 13.x, whose anonymous mode fails to lazy-load panel plugins — every panel (including Grafana's own) renders blank with "Loading plugin panel…". It is not the dashboard JSON. Log in athttp://localhost:3000withadmin/adminand the panels render. Thetelemetry:dashboardsimporter already authenticates, so import works regardless.
Verify the whole loop from the shell
# 1. Make a request and grab the trace id the package returns.
TID=$(curl -si http://localhost:8000/ | awk 'tolower($1)=="x-trace-id:"{print $2}' | tr -d '\r')
# 2. Look it up in Tempo (give it a few seconds to ingest).
sleep 5
curl -s "http://localhost:3000/api/datasources/proxy/uid/tempo/api/traces/$TID" \
-u admin:admin | jq '.batches[].scopeSpans[].spans[].name'
# 3. Confirm metrics landed in Prometheus.
curl -s "http://localhost:3000/api/datasources/proxy/uid/prometheus/api/v1/label/__name__/values" \
-u admin:admin | jq '.data[] | select(startswith("http_server"))'
In CI — an ephemeral stack for integration tests
The --group=redis / OTLP integration tests need real backing services.
Stand the stack up as a GitHub Actions service and tear it down with the
job:
jobs:
integration:
runs-on: ubuntu-latest
services:
redis:
image: redis:7
ports: ["6379:6379"]
lgtm:
image: grafana/otel-lgtm:latest
ports: ["4318:4318", "3000:3000"]
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: "8.3" }
- run: composer install --no-interaction
- run: vendor/bin/pest --group=redis
env:
OTEL_EXPORTER_OTLP_ENDPOINT: http://localhost:4318
TELEMETRY_STORE: redis
For pure unit tests you need none of this — TELEMETRY_STORE=array plus
Telemetry::fake() covers assertions without any container.
Tear down
docker compose down # keep volumes
docker compose down -v # wipe metrics/traces/logs/dashboards too
# one-liner variant: just Ctrl-C (it was --rm)
Where to go next
- Production backends (self-hosted, Grafana Cloud, scrape vs push): The Grafana stack.
- Wire it into your own app with the quickstart, then drive traffic through your routes, queues and commands to see every signal type land.