Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/api/plane/settings/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

DEBUG_TOOLBAR_PATCH_SETTINGS = False

# Prometheus metrics settings
INSTALLED_APPS += ("django_prometheus",)
MIDDLEWARE.insert(0, "django_prometheus.middleware.PrometheusBeforeMiddleware")
MIDDLEWARE.append("django_prometheus.middleware.PrometheusAfterMiddleware")

# Only show emails in console don't send it to smtp
EMAIL_BACKEND = os.environ.get("EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend")

Expand Down
1 change: 1 addition & 0 deletions apps/api/plane/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
path("api/instances/", include("plane.license.urls")),
path("api/v1/", include("plane.api.urls")),
path("auth/", include("plane.authentication.urls")),
path("", include("django_prometheus.urls")),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether nginx or ingress configs expose the api's / or /metrics path externally
rg -n "metrics" -g '*.conf' -g 'nginx*' -g 'docker-compose*.yml'

Repository: makeplane/plane

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map likely deployment and routing files, then inspect the relevant ones.
git ls-files | rg '(^|/)(urls\.py|docker-compose.*\.yml|docker-compose.*\.yaml|nginx.*|.*\.conf|.*ingress.*|.*helm.*|values.*\.ya?ml)$'

echo
echo '--- urls.py references to django_prometheus ---'
rg -n "django_prometheus|metrics" apps api . --glob '!*node_modules/*' --glob '!*dist/*' --glob '!*build/*'

echo
echo '--- compose / ingress / nginx references to api exposure ---'
rg -n "api:|ports:|expose:|django_prometheus|metrics|ingress|nginx|proxy_pass|server_name|host:" \
  docker-compose* . \
  --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/.git/**'

Repository: makeplane/plane

Length of output: 21403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- apps/api/plane/urls.py ---'
sed -n '1,120p' apps/api/plane/urls.py

echo
echo '--- docker-compose.yml ---'
sed -n '1,240p' docker-compose.yml

echo
echo '--- docker-compose-local.yml ---'
sed -n '1,240p' docker-compose-local.yml

echo
echo '--- apps/admin/nginx/nginx.conf ---'
sed -n '1,220p' apps/admin/nginx/nginx.conf

echo
echo '--- apps/space/nginx/nginx.conf ---'
sed -n '1,220p' apps/space/nginx/nginx.conf

echo
echo '--- apps/web/nginx/nginx.conf ---'
sed -n '1,220p' apps/web/nginx/nginx.conf

echo
echo '--- deployments/prometheus/prometheus.yml ---'
sed -n '1,220p' deployments/prometheus/prometheus.yml

Repository: makeplane/plane

Length of output: 11941


/metrics is exposed without auth in apps/api/plane/urls.py:23.

django_prometheus.urls is mounted at the root, and docker-compose-local.yml publishes api:8000 to the host, so these internal request/DB metrics are reachable by anyone who can hit that port. Lock /metrics to the internal network or add proxy auth if the path is forwarded in production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/urls.py` at line 23, The Prometheus metrics endpoint is
currently mounted publicly through the root include in the URL configuration, so
update the URL wiring in the `urlpatterns` setup to stop exposing
`django_prometheus.urls` to unauthenticated external callers. Move the metrics
route behind an internal-only path or protect it with access control at the
proxy/app layer, and make sure the change is applied where the
`include("django_prometheus.urls")` entry is registered.

path("", include("plane.web.urls")),
]

Expand Down
3 changes: 3 additions & 0 deletions apps/api/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ opentelemetry-exporter-otlp-proto-grpc==1.28.1
drf-spectacular==0.28.0
# html sanitizer
nh3==0.2.18

# prometheus metrics
django-prometheus==2.3.1
118 changes: 118 additions & 0 deletions deployments/grafana/dashboards/plane-dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"title": "API Request Rate (per Second)",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"editorMode": "code",
"expr": "sum(rate(django_http_requests_total_by_view_transport_method_total[5m])) by (view)",
"legendFormat": "{{view}}",
"range": true,
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth"
},
"unit": "reqps"
}
}
},
{
"collapsed": false,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"title": "Average API Latency (seconds)",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"editorMode": "code",
"expr": "sum(rate(django_http_requests_latency_seconds_by_view_method_sum[5m])) by (view) / sum(rate(django_http_requests_latency_seconds_by_view_method_count[5m])) by (view)",
"legendFormat": "{{view}}",
"range": true,
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth"
},
"unit": "s"
}
}
}
],
"refresh": "5s",
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-15m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Plane API Analytics",
"uid": "plane-api-analytics",
"version": 1,
"weekStart": ""
}
11 changes: 11 additions & 0 deletions deployments/grafana/provisioning/dashboards/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: 1

providers:
- name: 'Plane Dashboards'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards
8 changes: 8 additions & 0 deletions deployments/grafana/provisioning/datasources/datasource.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: 1

datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
Comment on lines +3 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== datasource.yml ==\n'
cat -n deployments/grafana/provisioning/datasources/datasource.yml

printf '\n== dashboard refs ==\n'
python3 - <<'PY'
import json, re, pathlib
p = pathlib.Path('deployments/grafana/dashboards/plane-dashboard.json')
data = json.loads(p.read_text())
text = p.read_text()

# Find occurrences of datasource references around uid/name keys
for key in ['uid', 'name']:
    print(f"\nOccurrences of '{key}':")
    for m in re.finditer(rf'"{key}"\s*:\s*"[^"]+"', text):
        start = max(0, m.start()-120)
        end = min(len(text), m.end()+120)
        snippet = text[start:end]
        print(snippet.replace('\n', ' '))
PY

Repository: makeplane/plane

Length of output: 2195


🌐 Web query:

Grafana provisioning datasource uid auto-generated if omitted dashboard datasource uid references Grafana 8.3

💡 Result:

When you omit the uid field in a Grafana datasource provisioning file, Grafana automatically generates a random UID for that datasource [1][2][3]. Starting in Grafana 8.3, Grafana shifted from referencing datasources by name to referencing them by UID within dashboard JSON definitions [4][5][6]. Because of this change, failing to explicitly specify a uid in your provisioning file can lead to significant issues [4][5]. If the UID is omitted, Grafana generates a new, random UID each time the system is redeployed or restarted [4][5]. Consequently, existing dashboards—which store a reference to a specific UID—will fail to find the datasource because the UID has changed, effectively "bricking" the dashboards [5][7][8]. To ensure stability and portability of dashboards across different Grafana environments (such as development, testing, and production), it is highly recommended to explicitly define a static, consistent uid for every provisioned datasource [4][7][9]. Example of explicit UID provisioning: datasources: - name: MyDataSource type: postgres uid: my_consistent_uid_001 #... other configuration options By manually setting the uid, you ensure that dashboard references remain valid across deployments, even if you rename the datasource later [4][6].

Citations:


🌐 Web query:

site:grafana.com datasource provisioning uid Grafana dashboard uid reference Prometheus

💡 Result:

When provisioning data sources in Grafana, you can explicitly define a unique identifier (UID) in your configuration file to ensure it remains consistent across different environments [1][2][3]. This UID is critical for dashboards, as they must reference this identifier to correctly link to the intended data source [4][5]. To provision a data source with a specific UID, include the uid field in your YAML provisioning file: apiVersion: 1 datasources: - name: My Prometheus type: prometheus uid: my-prometheus-uid # Explicitly defined UID access: proxy url: http://prometheus:9090 In your dashboard JSON files, the datasource field can reference this data source in two primary ways: 1. By Name: You can use the name of the data source as a string (e.g., "datasource": "My Prometheus") [5]. 2. By UID: You can use an object that specifies the type and the UID, or simply the UID string depending on your Grafana version and specific configuration, to ensure the dashboard points to the exact data source regardless of its display name [4][5]. To avoid issues where dashboards fail to find the data source (especially during migrations or when moving between systems), it is best practice to always provide a static, explicit UID in your data source provisioning YAML rather than allowing Grafana to generate a random one [1][5]. If you are exporting dashboards for use across different systems, ensuring the data source UID is identical in both environments prevents broken references [6][2]. Note that in recent versions of Grafana, there has been a move toward enforcing stricter UID formats to improve consistency [7]. Always verify that your defined UID is unique within your Grafana organization [2].

Citations:


Add a stable uid to the Prometheus datasource
deployments/grafana/dashboards/plane-dashboard.json binds panels to uid: "Prometheus", but this provisioning file lets Grafana generate a different UID on startup. Add uid: Prometheus here so the dashboard keeps resolving the datasource.

🔧 Proposed fix
 datasources:
   - name: Prometheus
     type: prometheus
     access: proxy
     url: http://prometheus:9090
+    uid: Prometheus
     isDefault: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
uid: Prometheus
isDefault: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deployments/grafana/provisioning/datasources/datasource.yml` around lines 3 -
8, The Prometheus datasource provisioning in datasource.yml is missing a fixed
uid, which can break dashboard bindings that reference the datasource by uid.
Update the datasource entry to set the uid explicitly to Prometheus so the
provisioned datasource matches the references used by plane-dashboard.json and
remains stable across Grafana restarts.

9 changes: 9 additions & 0 deletions deployments/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global:
scrape_interval: 5s
evaluation_interval: 5s

scrape_configs:
- job_name: "plane-api"
metrics_path: "/metrics"
static_configs:
- targets: ["api:8000"]
33 changes: 33 additions & 0 deletions docker-compose-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,44 @@ services:
- plane-db
- plane-redis

prometheus:
image: prom/prometheus:latest
restart: unless-stopped
volumes:
- ./deployments/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
ports:
- "9091:9090"
networks:
- dev_env

grafana:
image: grafana/grafana:latest
restart: unless-stopped
ports:
- "3002:3000"
volumes:
- ./deployments/grafana/provisioning:/etc/grafana/provisioning
- ./deployments/grafana/dashboards:/var/lib/grafana/dashboards
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
networks:
- dev_env
depends_on:
- prometheus

volumes:
redisdata:
uploads:
pgdata:
rabbitmq_data:
prometheus_data:
grafana_data:

networks:
dev_env:
Expand Down