Local Stack
One Docker Compose command to run Prometheus, Grafana, Loki, and Tempo locally — with all data sources pre-wired.
Local Observability Stack
This guide sets up a complete local observability environment in a single docker compose up. You get:
- Prometheus — scrapes your app's
/metricsendpoint every 15 seconds - Grafana — dashboards for metrics, logs, and traces, all pre-connected
- Loki — collects your app's structured log output
- Promtail — ships log files from
var/log/into Loki - Tempo — stores distributed traces from OpenTelemetry
This is the fastest way to see Vortos observability working end to end before choosing production tooling.
Prerequisites
- Docker and Docker Compose installed
- Vortos app running locally (any port)
- Metrics adapter set to Prometheus (see Step 3 below)
Step 1 — Create the Docker config directory
In your project root:
mkdir -p docker/grafana/provisioning/datasources
mkdir -p docker/grafana/provisioning/dashboardsStep 2 — Write the Docker Compose file
services:
prometheus:
image: prom/prometheus:v2.51.0
ports:
- "9090:9090"
volumes:
- ./docker/prometheus.yml:/etc/prometheus/prometheus.yml:ro
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=7d
grafana:
image: grafana/grafana-oss:10.4.0
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
GF_AUTH_ANONYMOUS_ENABLED: "false"
volumes:
- ./docker/grafana/provisioning:/etc/grafana/provisioning:ro
- grafana_data:/var/lib/grafana
depends_on: [prometheus, loki, tempo]
loki:
image: grafana/loki:2.9.0
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:2.9.0
volumes:
- ./var/log:/var/log:ro
- ./docker/promtail.yml:/etc/promtail/config.yml:ro
command: -config.file=/etc/promtail/config.yml
depends_on: [loki]
tempo:
image: grafana/tempo:2.4.0
ports:
- "4318:4318" # OTLP HTTP — your app sends traces here
- "3200:3200" # Tempo query API (used by Grafana)
volumes:
- ./docker/tempo.yaml:/etc/tempo.yaml:ro
- tempo_data:/var/tempo
command: -config.file=/etc/tempo.yaml
volumes:
grafana_data:
tempo_data:Step 3 — Prometheus scrape config
global:
scrape_interval: 15s
scrape_configs:
- job_name: vortos-app
static_configs:
- targets: ["host.docker.internal:8000"] # your app's port
bearer_token: "local-dev-token"
metrics_path: /metricshost.docker.internal
host.docker.internal resolves to your machine from inside Docker. On Linux, add --add-host=host.docker.internal:host-gateway to the Prometheus service, or replace with your machine's local IP.
Step 4 — Promtail log shipping config
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: vortos-logs
static_configs:
- targets: [localhost]
labels:
job: vortos
__path__: /var/log/*.log
pipeline_stages:
- json:
expressions:
level: level_name
channel: channel
message: message
- labels:
level:
channel:This reads every .log file from var/log/, parses the JSON fields, and exposes level and channel as Loki labels so you can filter by them in Grafana.
Step 5 — Tempo config
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
storage:
trace:
backend: local
local:
path: /var/tempo/blocks
compactor:
compaction:
block_retention: 48hStep 6 — Grafana data sources (auto-provisioned)
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
editable: false
- name: Loki
type: loki
url: http://loki:3100
editable: false
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: '"trace_id":"(\w+)"'
name: TraceID
url: "$${__value.raw}"
- name: Tempo
type: tempo
url: http://tempo:3200
uid: tempo
editable: false
jsonData:
tracesToLogsV2:
datasourceUid: loki
filterByTraceID: trueThe derivedFields entry makes trace IDs in logs clickable — clicking a trace_id value in Loki jumps directly to the matching trace in Tempo.
Step 7 — Configure Vortos
Enable Prometheus metrics:
use Vortos\Metrics\Config\MetricsAdapter;
use Vortos\Metrics\DependencyInjection\VortosMetricsConfig;
return static function (VortosMetricsConfig $config): void {
$config->adapter(MetricsAdapter::Prometheus)
->prometheusEndpointToken($_ENV['METRICS_TOKEN'] ?? 'local-dev-token');
};Add to .env:
METRICS_TOKEN=local-dev-token
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
OTEL_SERVICE_NAME=myappEnable OpenTelemetry tracing (optional but recommended — see Tracing docs):
composer require open-telemetry/sdk open-telemetry/exporter-otlpStep 8 — Start the stack
docker compose -f docker/observability.yml up -dStep 9 — Verify everything is connected
Prometheus:
open http://localhost:9090/targets
# vortos-app should show State: UPGrafana:
open http://localhost:3000
# Login: admin / admin
# Go to Explore → select Prometheus → run: vortos_http_requests_totalLoki:
# In Grafana → Explore → select Loki
# Run: {job="vortos"} | json
# You should see your app's log linesTempo:
# Make a few requests to your app
# In Grafana → Explore → select Tempo → Search
# Traces should appear within secondsTrace → Log jump:
# In Loki, find a log line with a trace_id field
# Click the trace_id value — it should open the trace in TempoTear down
docker compose -f docker/observability.yml down -v # -v removes volumes (data)
docker compose -f docker/observability.yml down # keep data volumesDeploy Markers & SLO Resources
Annotate dashboards with deploy/rollback events linked to the exact build, and declare SLOs as validated, version-controlled resources instead of free-floating dashboard config.
Sentry
Capture PHP exceptions and error-level logs in Sentry — step-by-step setup and Vortos configuration.