From 62d4508ef1761d1eb5a5aaa93ddda67cd375be69 Mon Sep 17 00:00:00 2001 From: Ekpemark Date: Thu, 30 Jul 2026 01:10:23 +0100 Subject: [PATCH] feat(devops): add observability compose profile --- .../provisioning/dashboards/dashboard.yml | 11 +++++ .../dashboards/sorokeep-overview.json | 39 +++++++++++++++ .../provisioning/datasources/prometheus.yml | 10 ++++ devops/prometheus/prometheus.yml | 9 ++++ docker-compose.observability.yml | 47 +++++++++++++++++++ docs/observability.md | 9 ++++ tests/docker/docker-compose.test.ts | 36 ++++++++++++++ 7 files changed, 161 insertions(+) create mode 100644 devops/grafana/provisioning/dashboards/dashboard.yml create mode 100644 devops/grafana/provisioning/dashboards/sorokeep-overview.json create mode 100644 devops/grafana/provisioning/datasources/prometheus.yml create mode 100644 devops/prometheus/prometheus.yml create mode 100644 docker-compose.observability.yml create mode 100644 docs/observability.md diff --git a/devops/grafana/provisioning/dashboards/dashboard.yml b/devops/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 00000000..6f31a9e7 --- /dev/null +++ b/devops/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: Sorokeep + orgId: 1 + folder: Sorokeep + type: file + disableDeletion: true + editable: false + options: + path: /etc/grafana/provisioning/dashboards diff --git a/devops/grafana/provisioning/dashboards/sorokeep-overview.json b/devops/grafana/provisioning/dashboards/sorokeep-overview.json new file mode 100644 index 00000000..18662357 --- /dev/null +++ b/devops/grafana/provisioning/dashboards/sorokeep-overview.json @@ -0,0 +1,39 @@ +{ + "annotations": { "list": [] }, + "editable": false, + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { "options": { "0": { "text": "Down" }, "1": { "text": "Up" } }, "type": "value" } + ], + "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] } + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 0 }, + "id": 1, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ { "expr": "up{job=\"sorokeep\"}", "refId": "A" } ], + "title": "Sorokeep Metrics Endpoint", + "type": "stat" + } + ], + "schemaVersion": 39, + "tags": [ "sorokeep", "observability" ], + "templating": { "list": [] }, + "time": { "from": "now-15m", "to": "now" }, + "title": "Sorokeep Overview", + "uid": "sorokeep-overview", + "version": 1 +} diff --git a/devops/grafana/provisioning/datasources/prometheus.yml b/devops/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 00000000..00f99157 --- /dev/null +++ b/devops/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + uid: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/devops/prometheus/prometheus.yml b/devops/prometheus/prometheus.yml new file mode 100644 index 00000000..f9c270da --- /dev/null +++ b/devops/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: sorokeep + static_configs: + - targets: + - sorokeep:9464 diff --git a/docker-compose.observability.yml b/docker-compose.observability.yml new file mode 100644 index 00000000..aebc1b30 --- /dev/null +++ b/docker-compose.observability.yml @@ -0,0 +1,47 @@ +# Prometheus and Grafana overlay for local Sorokeep observability. +# +# Usage: +# docker compose -f docker-compose.yaml -f docker-compose.observability.yml --profile observability up + +services: + sorokeep: + expose: + - "9464" + + prometheus: + image: prom/prometheus:v3.3.0 + profiles: ["observability"] + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.enable-lifecycle" + ports: + - "${PROMETHEUS_PORT:-9090}:9090" + volumes: + - ./devops/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + restart: unless-stopped + networks: + - sorokeep-network + + grafana: + image: grafana/grafana:11.6.0 + profiles: ["observability"] + depends_on: + - prometheus + environment: + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin} + GF_USERS_ALLOW_SIGN_UP: "false" + ports: + - "${GRAFANA_PORT:-3000}:3000" + volumes: + - grafana-data:/var/lib/grafana + - ./devops/grafana/provisioning:/etc/grafana/provisioning:ro + restart: unless-stopped + networks: + - sorokeep-network + +volumes: + prometheus-data: + grafana-data: diff --git a/docs/observability.md b/docs/observability.md new file mode 100644 index 00000000..7b873611 --- /dev/null +++ b/docs/observability.md @@ -0,0 +1,9 @@ +# Observability + +Run Sorokeep with its local Prometheus and Grafana stack using the observability overlay: + +```sh +docker compose -f docker-compose.yaml -f docker-compose.observability.yml --profile observability up +``` + +Prometheus is available at `http://localhost:9090` and Grafana at `http://localhost:3000`. Grafana uses `admin` / `admin` by default (override them with `GRAFANA_ADMIN_USER` and `GRAFANA_ADMIN_PASSWORD`) and provisions the **Sorokeep Overview** dashboard automatically. Prometheus scrapes the Sorokeep metrics endpoint at `sorokeep:9464` within the Compose network. diff --git a/tests/docker/docker-compose.test.ts b/tests/docker/docker-compose.test.ts index fdda0920..911230a9 100644 --- a/tests/docker/docker-compose.test.ts +++ b/tests/docker/docker-compose.test.ts @@ -5,14 +5,25 @@ import YAML from "yaml"; const ROOT = path.resolve(import.meta.dirname, "../.."); const COMPOSE_FILE = path.join(ROOT, "docker-compose.yaml"); +const OBSERVABILITY_COMPOSE_FILE = path.join(ROOT, "docker-compose.observability.yml"); +const PROMETHEUS_CONFIG_FILE = path.join(ROOT, "devops/prometheus/prometheus.yml"); +const GRAFANA_DASHBOARD_PROVIDER_FILE = path.join( + ROOT, + "devops/grafana/provisioning/dashboards/dashboard.yml", +); +const GRAFANA_DASHBOARD_FILE = path.join(ROOT, "devops/grafana/provisioning/dashboards/sorokeep-overview.json"); let composeConfig: any; +let observabilityComposeConfig: any; beforeAll(() => { if (fs.existsSync(COMPOSE_FILE)) { const raw = fs.readFileSync(COMPOSE_FILE, "utf8"); composeConfig = YAML.parse(raw); } + if (fs.existsSync(OBSERVABILITY_COMPOSE_FILE)) { + observabilityComposeConfig = YAML.parse(fs.readFileSync(OBSERVABILITY_COMPOSE_FILE, "utf8")); + } }); describe("docker-compose.yaml configuration", () => { @@ -143,3 +154,28 @@ describe("docker-compose.yaml configuration", () => { }); }); }); + +describe("observability compose overlay", () => { + it("defines profiled Prometheus and Grafana services", () => { + expect(fs.existsSync(OBSERVABILITY_COMPOSE_FILE)).toBe(true); + expect(observabilityComposeConfig.services.prometheus.profiles).toContain("observability"); + expect(observabilityComposeConfig.services.grafana.profiles).toContain("observability"); + }); + + it("wires Prometheus to Sorokeep's metrics endpoint", () => { + expect(fs.existsSync(PROMETHEUS_CONFIG_FILE)).toBe(true); + const prometheusConfig = YAML.parse(fs.readFileSync(PROMETHEUS_CONFIG_FILE, "utf8")); + const targets = prometheusConfig.scrape_configs.flatMap((job: any) => + job.static_configs.flatMap((config: any) => config.targets), + ); + expect(targets).toContain("sorokeep:9464"); + }); + + it("provisions the Sorokeep Grafana dashboard", () => { + expect(fs.existsSync(GRAFANA_DASHBOARD_PROVIDER_FILE)).toBe(true); + expect(fs.existsSync(GRAFANA_DASHBOARD_FILE)).toBe(true); + const dashboard = JSON.parse(fs.readFileSync(GRAFANA_DASHBOARD_FILE, "utf8")); + expect(dashboard.title).toBe("Sorokeep Overview"); + expect(dashboard.panels.length).toBeGreaterThan(0); + }); +});