Skip to content

Grafana

Grafana is the main visualization tool of PerfShop. It aggregates the four telemetry sinks (Prometheus, Loki, Tempo, Pyroscope), exposes 10 shipped dashboards (5 Students + 5 Instructors) with a differentiated access policy, and allows interactive exploration in Explore mode.

Source of truth

This page is taken from the grafana block in the compose files, the provisioning files grafana/provisioning/datasources/*.yml, grafana/provisioning/dashboards/dashboards.yml, and the initialization script grafana-seed/seed.py.

Pinned version

PerfShop pins Grafana to version 12.0.0 (image grafana/grafana:12.0.0). The other observability components use latest; Grafana is the exception, because:

  • The 10 shipped dashboards rely on panel features (flamegraph, traces table, TraceQL editor, auto-units format) introduced in Grafana 11+ and stabilized in 12.
  • The Python seed uses the legacy API POST /api/folders/{uid}/permissions which could evolve in a future major version.
  • The Pyroscope and Tempo datasources depend on native plugins whose configuration format may change.

Upgrading to a later version will require manual validation against the 10 dashboards.

Configuration via environment variables

environment:
  - GF_SECURITY_ADMIN_USER=admin
  - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-perfshop}
  - GF_USERS_ALLOW_SIGN_UP=false
  - GF_SERVER_ROOT_URL=${PUBLIC_GRAFANA_URL:-http://localhost:3002}
  - GF_PANELS_DISABLE_SANITIZE_HTML=true
  - GF_AUTH_ANONYMOUS_ENABLED=true
  - GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
  - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
  - GF_USERS_DEFAULT_LANGUAGE=${PERFSHOP_UI_LOCALE:-en-US}
Variable Effect
GF_SECURITY_ADMIN_USER / _PASSWORD Grafana superadmin account, used by the seed and by the instructor
GF_USERS_ALLOW_SIGN_UP=false Disables self-signup from the UI
GF_SERVER_ROOT_URL Public URL used for absolute links (emails, sharing, OAuth)
GF_PANELS_DISABLE_SANITIZE_HTML=true Allows Text panels to use raw HTML (used by the "Guide" panels of the APM dashboards)
GF_AUTH_ANONYMOUS_ENABLED=true Anonymous access enabled — the key of the pedagogical strategy
GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer Anonymous users only get the Viewer role (read-only)
GF_USERS_DEFAULT_LANGUAGE Grafana UI locale (fr-FR or en-US), driven by PERFSHOP_UI_LOCALE

The internal management port of Grafana is 3000, exposed on host port 3002 by default (GRAFANA_HTTP_PORT variable).

The Docker healthcheck queries /api/health every 10 seconds with start_period: 30s:

healthcheck:
  test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
  interval: 10s
  timeout: 5s
  retries: 10
  start_period: 30s

Provisioned datasources

Grafana automatically reads the grafana/provisioning/datasources/ folder at startup and declares four datasources, each with a stable UID used by the JSON dashboards.

flowchart LR
  G["Grafana 12.0.0"]
  G --> P["uid: prometheus<br/>http://prometheus:9090<br/>(default)"]
  G --> L["uid: loki<br/>http://perfshop-loki:3100"]
  G --> T["uid: tempo<br/>http://perfshop-tempo:3200"]
  G --> PY["uid: pyroscope<br/>http://perfshop-pyroscope:4040"]

Prometheus datasource

- name: Prometheus
  type: prometheus
  uid: prometheus
  access: proxy
  url: http://prometheus:9090
  isDefault: true
  editable: true
  jsonData:
    timeInterval: "5s"
  • isDefault: true: this is the datasource selected by default in the panel editor and in Explore.
  • timeInterval: "5s": tells Grafana about the actual scrape_interval so that rate() and irate() functions are properly calibrated when the user does not specify the window.

Loki datasource

- name: Loki
  type: loki
  uid: loki
  access: proxy
  url: http://perfshop-loki:3100
  isDefault: false
  editable: true
  jsonData:
    maxLines: 1000
    timeout: 60
  • maxLines: 1000: limits the number of lines returned by default in log panels (can be overridden per panel).
  • timeout: 60: query timeout on the Grafana side, in seconds.

Tempo datasource

- name: Tempo
  type: tempo
  uid: tempo
  access: proxy
  url: http://perfshop-tempo:3200
  isDefault: false
  editable: true
  jsonData:
    httpMethod: GET
    tracesToLogsV2:
      datasourceUid: loki
      spanStartTimeShift: "-1m"
      spanEndTimeShift: "1m"
      tags:
        - key: service.name
          value: app
      filterByTraceID: true
      filterBySpanID: false
      customQuery: false
    tracesToMetrics:
      datasourceUid: prometheus
      spanStartTimeShift: "-1m"
      spanEndTimeShift: "1m"
      tags:
        - key: service.name
          value: app
    serviceMap:
      datasourceUid: prometheus
    search:
      hide: false
    nodeGraph:
      enabled: true
    lokiSearch:
      datasourceUid: loki

This is the most richly configured datasource. Three cross-correlations are enabled:

  • tracesToLogsV2 → Loki: from any span in the trace view, you can click to open Loki with a ±1 min time filter around the span and an automatic traceID filter.
  • tracesToMetrics → Prometheus: from a span, you can display the Prometheus metrics for the operation over the same time window.
  • serviceMap: uses the traces_service_graph_* metrics produced by Tempo's metrics_generator and stored in Prometheus to generate the service map.
  • nodeGraph: enabled: true: enables the graph visualization for traces.
  • lokiSearch: enables searching for correlated logs directly from the Tempo editor.

Pyroscope datasource

- name: Pyroscope
  type: grafana-pyroscope-datasource
  uid: pyroscope
  access: proxy
  url: http://perfshop-pyroscope:4040
  isDefault: false
  editable: true

Minimal configuration — Pyroscope has no special correlations to configure; the native Grafana plugin handles everything.

Folders and dashboard provisioning

Dashboards are organized into two distinct folders in Grafana, with two separate provisioning providers.

# grafana/provisioning/dashboards/dashboards.yml
apiVersion: 1
providers:
  - name: 'PerfShop Dashboards  Élèves'
    orgId: 1
    folder: 'Élèves'
    folderUid: 'perfshop-eleves'
    type: file
    disableDeletion: false
    updateIntervalSeconds: 10
    allowUiUpdates: true
    options:
      path: /etc/grafana/dashboards/eleves

  - name: 'PerfShop Dashboards  Formateurs'
    orgId: 1
    folder: 'Formateurs'
    folderUid: 'perfshop-formateurs'
    type: file
    disableDeletion: false
    updateIntervalSeconds: 10
    allowUiUpdates: true
    options:
      path: /etc/grafana/dashboards/formateurs
Aspect Students folder Instructors folder
folderUid perfshop-eleves perfshop-formateurs
Source path ./grafana/dashboards/eleves/ ./grafana/dashboards/formateurs/
Number of dashboards 5 5
ACL after seed Inherits the Viewer role (visible without login) Admin only (set by the seed)
updateIntervalSeconds 10 (Grafana re-reads files every 10 s) 10
allowUiUpdates true true
disableDeletion false false

The bind mounts in compose:

volumes:
  - grafana-data:/var/lib/grafana
  - ./grafana/provisioning:/etc/grafana/provisioning
  - ./grafana/dashboards:/etc/grafana/dashboards

Access strategy — the key element

PerfShop cleanly separates two user populations on Grafana:

flowchart TB
  V["Anonymous user<br/>(no login)"]
  A["Logged-in admin<br/>(login admin/perfshop)"]

  subgraph G["Grafana"]
    direction TB
    FE["Students folder<br/>(Viewer visible)"]
    FF["Instructors folder<br/>(Admin-only ACL)"]
  end

  V -->|GF_AUTH_ANONYMOUS_ENABLED=true<br/>Role=Viewer| FE
  V -.-|❌ HTTP 403| FF
  A -->|✅| FE
  A -->|✅| FF

How it works in practice

  1. GF_AUTH_ANONYMOUS_ENABLED=true + GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer declared as environment variables → any visitor reaching the Grafana URL is automatically authenticated as an anonymous user with the Viewer role in the "Main Org." organization.
  2. By default, Grafana applies organizational inheritance: a Viewer sees all folders in their org.
  3. The perfshop-grafana-seed seed applies an explicit ACL on the Instructors folder: Admin only. This ACL replaces the Viewer inheritance for this folder — as a result, anonymous users see the Students folder but not the Instructors folder.

The grafana-seed/seed.py seed

The perfshop-grafana-seed service is a python:3.11-slim container that:

  1. Installs requests==2.31.0 at startup.
  2. Waits for Grafana to respond on /api/health (retry loop, timeout 7m30).
  3. Checks that the Instructors folder is properly provisioned (retry loop on GET /api/folders/{uid}).
  4. Sets the Admin-only ACL via POST /api/folders/perfshop-formateurs/permissions:
    { "items": [{ "role": "Admin", "permission": 4 }] }
    
    permission: 4 = Admin (1=View, 2=Edit, 4=Admin). A single item is enough: the API replaces the entire existing ACL.
  5. Verifies that the ACL is effective by making a call without authentication on /api/folders/perfshop-formateurs — must return 401 or 403.
  6. Sets the home dashboard to perfshop-general-v1 ("General Containers View") via PATCH /api/org/preferences with {"homeDashboardUID": "perfshop-general-v1"}.

The seed is marked restart: "no" (one-shot) and depends_on: grafana: condition: service_healthy.

sequenceDiagram
  autonumber
  participant S as perfshop-grafana-seed
  participant G as Grafana

  S->>G: GET /api/health<br/>(loop 5s × 90)
  G-->>S: 200 OK

  S->>G: GET /api/folders/perfshop-formateurs<br/>(admin auth)<br/>(loop 5s × 24)
  G-->>S: 200 (folder provisioned)

  S->>G: POST /api/folders/perfshop-formateurs/permissions<br/>{"items":[{"role":"Admin","permission":4}]}
  G-->>S: 200 OK

  S->>G: GET /api/folders/perfshop-formateurs<br/>(WITHOUT auth)
  G-->>S: 401 or 403
  Note over S: ✓ ACL effective

  S->>G: PATCH /api/org/preferences<br/>{"homeDashboardUID":"perfshop-general-v1"}
  G-->>S: 200 OK

  Note over S: Exit code 0

Seed idempotency

The seed can be re-run manually at any time (docker compose up perfshop-grafana-seed) without side effects: POST /api/folders/{uid}/permissions replaces the entire ACL, and PATCH /api/org/preferences is just an update.

Volumes and persistent data

Volume Mount Content
grafana-data (named volume) /var/lib/grafana Grafana SQLite database, user sessions, installed plugins, manually created dashboards, custom ACLs
./grafana/provisioning (bind mount) /etc/grafana/provisioning Provisioning files for datasources and dashboards (read-only in practice)
./grafana/dashboards (bind mount) /etc/grafana/dashboards 10 shipped JSON files (Students + Instructors)

Modifying a shipped dashboard

The shipped JSON dashboards are reloaded every 10 s by Grafana (updateIntervalSeconds: 10). A change made in the UI is kept for 10 s then overwritten by the JSON file content. To durably modify a shipped dashboard, edit the corresponding JSON file in grafana/dashboards/{eleves|formateurs}/ and repackage it in the deployment.

To go further