Vortos
Integrations

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 /metrics endpoint 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/dashboards

Step 2 — Write the Docker Compose file

docker/observability.yml
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

docker/prometheus.yml
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: /metrics

host.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

docker/promtail.yml
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

docker/tempo.yaml
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: 48h

Step 6 — Grafana data sources (auto-provisioned)

docker/grafana/provisioning/datasources/datasources.yaml
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: true

The 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:

config/metrics.php
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=myapp

Enable OpenTelemetry tracing (optional but recommended — see Tracing docs):

composer require open-telemetry/sdk open-telemetry/exporter-otlp

Step 8 — Start the stack

docker compose -f docker/observability.yml up -d

Step 9 — Verify everything is connected

Prometheus:

open http://localhost:9090/targets
# vortos-app should show State: UP

Grafana:

open http://localhost:3000
# Login: admin / admin
# Go to Explore → select Prometheus → run: vortos_http_requests_total

Loki:

# In Grafana → Explore → select Loki
# Run: {job="vortos"} | json
# You should see your app's log lines

Tempo:

# Make a few requests to your app
# In Grafana → Explore → select Tempo → Search
# Traces should appear within seconds

Trace → Log jump:

# In Loki, find a log line with a trace_id field
# Click the trace_id value — it should open the trace in Tempo

Tear down

docker compose -f docker/observability.yml down -v  # -v removes volumes (data)
docker compose -f docker/observability.yml down      # keep data volumes

On this page