Repository navigation
feat(devops): add observability compose profile #594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| apiVersion: 1 | ||
|
|
||
| datasources: | ||
| - name: Prometheus | ||
| uid: prometheus | ||
| type: prometheus | ||
| access: proxy | ||
| url: http://prometheus:9090 | ||
| isDefault: true | ||
| editable: false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| global: | ||
| scrape_interval: 15s | ||
| evaluation_interval: 15s | ||
|
|
||
| scrape_configs: | ||
| - job_name: sorokeep | ||
| static_configs: | ||
| - targets: | ||
| - sorokeep:9464 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| }); | ||
|
Comment on lines
+158
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win Validate the merged Compose configuration, not only individual files. These tests never run 🧰 Tools🪛 ast-grep (0.45.0)[warning] 166-166: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use. (detect-non-literal-fs-filename-typescript) [warning] 176-176: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use. (detect-non-literal-fs-filename-typescript) 🤖 Prompt for AI Agents |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: AbdulmalikAlayande/sorokeep
Length of output: 480
🌐 Web query:
Docker Compose ports short syntax published on all interfaces host IP 127.0.0.1 documentation💡 Result:
In Docker Compose, the short syntax for ports follows the format [HOST:]CONTAINER[/PROTOCOL] [1][2][3]. If you do not specify a host IP in the short syntax, Docker Compose defaults to binding the port to all available network interfaces (0.0.0.0), which may expose the container to the public internet [1][4][3]. To restrict access so that the container is only reachable from the host machine, you must explicitly include 127.0.0.1 in the port mapping [1][4][3]. For example: ports: - "127.0.0.1:8080:80" This configuration maps the container's port 80 to the host's port 8080, but only on the 127.0.0.1 loopback address [1][2][3]. Key considerations include: - Security: Publishing to 0.0.0.0 by default can bypass host firewall rules if not properly managed [1][3]. - YAML Syntax: When using the short syntax with an IP address, it is recommended to wrap the entire string in quotes to prevent the YAML parser from misinterpreting the values (e.g., as a base-60 float) [2][5][6]. - Alternatives: If you need to avoid specifying the IP for every service, you can configure the Docker daemon to use 127.0.0.1 as the default binding address for published ports, though this is a global setting that affects all containers [4].
Citations:
🌐 Web query:
Prometheus --web.enable-lifecycle endpoint security unauthenticated POST /-/reload /-/quit documentation💡 Result:
When the --web.enable-lifecycle flag is enabled in Prometheus, the /-/reload and /-/quit endpoints become accessible via HTTP requests [1][2]. These endpoints are disabled by default [2][3]. Crucially, Prometheus does not natively enforce authentication on these lifecycle endpoints [1]. If this flag is enabled, any party with network access to the Prometheus HTTP server can trigger a configuration reload or a graceful shutdown of the process [1][4]. To mitigate the security risks associated with enabling these endpoints, the Prometheus documentation recommends the following: 1. Network Isolation: Ensure that the Prometheus server is not exposed to untrusted networks [1]. 2. Reverse Proxy Authentication: If you need to enable these endpoints but require authentication, use an external reverse proxy (e.g., Nginx, Apache, or a dedicated sidecar proxy) to intercept requests and enforce authentication before forwarding them to the Prometheus instance [1]. Alternatively, you can perform these actions without exposing the HTTP endpoints by using system signals: - Reload configuration: Send a SIGHUP signal to the Prometheus process [2][5]. - Shutdown: Send a SIGTERM signal to the Prometheus process [2][5]. Using these signals is the recommended, more secure alternative to using the HTTP lifecycle endpoints when local access to the server is available [2][5].
Citations:
🏁 Script executed:
Repository: AbdulmalikAlayande/sorokeep
Length of output: 2680
Keep the observability stack local by default.
Docker Compose short port mappings publish on all host interfaces, so the Prometheus lifecycle endpoint and Grafana login are reachable externally unless protected. Bind both defaults to
127.0.0.1and remove--web.enable-lifecycleunless lifecycle controls are required and wrapped in authentication/proxy controls; also require non-default Grafana admin credentials before enabling this overlay.Proposed hardening
command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - - "--web.enable-lifecycle" ... - - "${PROMETHEUS_PORT:-9090}:9090" + - "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090" ... - - "${GRAFANA_PORT:-3000}:3000" + - "127.0.0.1:${GRAFANA_PORT:-3000}:3000"Also applies to lines 33-37.
📝 Committable suggestion
🤖 Prompt for AI Agents