-
Notifications
You must be signed in to change notification settings - Fork 31
Add cross-org usage dashboard to Grafana #2476
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 3 commits
36a7500
812534b
fcc6b09
a7b8b02
10693f3
c27a710
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,263 @@ | ||
| # Cross-org usage dashboard, read straight from the `usage` table via the per-env | ||
| # read-only Postgres datasource. The in-app Usage page shows one org its own numbers; | ||
| # this shows the whole platform. | ||
| # | ||
| # Lives in base/ (not per-cluster) even though each env's datasource has a different | ||
| # name, because the datasource is picked by a template variable rather than hardcoded. | ||
| # The variable queries type "postgres", which is the `type` all three GrafanaDatasource | ||
| # CRs declare, so it matches whether or not this Grafana resolves the newer | ||
| # grafana-postgresql-datasource plugin alias. | ||
| # | ||
| # Periods are UTC-anchored calendar months (services/usage.py:_current_period), pinned at | ||
| # both ends: `timezone: utc` so rendering does not shift rows across month boundaries, and | ||
| # `now() AT TIME ZONE 'UTC'` in SQL so the month bounds do not depend on the DB session | ||
| # TimeZone, which no CNPG manifest sets. | ||
| apiVersion: grafana.integreatly.org/v1beta1 | ||
| kind: GrafanaDashboard | ||
| metadata: | ||
| name: rhesis-usage | ||
| namespace: monitoring | ||
| annotations: | ||
| argocd.argoproj.io/sync-wave: "2" | ||
| spec: | ||
| folder: Rhesis | ||
| instanceSelector: | ||
| matchLabels: | ||
| dashboards: grafana | ||
| json: | | ||
| { | ||
| "uid": "rhesis-usage", | ||
| "title": "Rhesis Usage", | ||
| "tags": ["rhesis", "usage"], | ||
| "timezone": "utc", | ||
| "editable": false, | ||
| "schemaVersion": 39, | ||
| "version": 1, | ||
| "refresh": "", | ||
| "time": { "from": "now-12M", "to": "now" }, | ||
| "templating": { | ||
| "list": [ | ||
| { | ||
| "name": "datasource", | ||
| "label": "Datasource", | ||
| "type": "datasource", | ||
| "query": "postgres", | ||
| "current": {}, | ||
| "hide": 0 | ||
| }, | ||
| { | ||
| "name": "resource", | ||
| "label": "Resource", | ||
| "type": "query", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "query": "SELECT DISTINCT resource FROM public.usage WHERE deleted_at IS NULL ORDER BY 1", | ||
| "refresh": 1, | ||
| "sort": 1, | ||
| "current": {}, | ||
| "hide": 0 | ||
| }, | ||
| { | ||
| "name": "org", | ||
| "label": "Organization", | ||
| "type": "query", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "query": "SELECT COALESCE(display_name, name) FROM public.organization WHERE deleted_at IS NULL ORDER BY 1", | ||
|
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. Improvement Org variable uses If two orgs share a display name/name (or a name contains quotes), series can merge and the
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. One extra reason to switch the org variable/value to ids: several panels also Using 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. Fixed now: org templating query returns |
||
| "refresh": 1, | ||
| "sort": 1, | ||
| "multi": true, | ||
| "includeAll": true, | ||
| "current": { "text": "All", "value": "$__all" }, | ||
| "hide": 0 | ||
| } | ||
| ] | ||
| }, | ||
| "panels": [ | ||
| { | ||
| "type": "row", | ||
| "title": "Current billing period", | ||
| "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, | ||
| "collapsed": false, | ||
| "panels": [] | ||
| }, | ||
| { | ||
| "id": 1, | ||
| "type": "stat", | ||
| "title": "Platform total this month", | ||
| "description": "Sum across every organization for the current calendar month. Ignores the time picker.", | ||
| "gridPos": { "h": 4, "w": 24, "x": 0, "y": 1 }, | ||
| "fieldConfig": { | ||
| "defaults": { "unit": "short", "decimals": 0, "thresholds": { "mode": "absolute", "steps": [{ "color": "text", "value": null }] } }, | ||
| "overrides": [] | ||
| }, | ||
| "options": { | ||
| "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, | ||
| "colorMode": "none", | ||
| "graphMode": "none", | ||
| "textMode": "auto", | ||
| "justifyMode": "auto" | ||
| }, | ||
| "targets": [ | ||
| { | ||
| "refId": "A", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "editorMode": "code", | ||
| "rawQuery": true, | ||
| "format": "table", | ||
| "rawSql": "SELECT\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'test_executions'), 0) AS \"Test executions\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'tracing_spans'), 0) AS \"Tracing spans\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'test_generation'), 0) AS \"Tests generated\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'model_tokens'), 0) AS \"Model tokens\"\nFROM public.usage u\nJOIN public.organization o ON o.id = u.organization_id\nWHERE u.deleted_at IS NULL\n AND o.deleted_at IS NULL\n AND u.period_start = date_trunc('month', now() AT TIME ZONE 'UTC')::date" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": 2, | ||
| "type": "table", | ||
| "title": "Usage by organization, this month", | ||
| "description": "One row per organization for the current calendar month. Ignores the time picker.", | ||
| "gridPos": { "h": 9, "w": 16, "x": 0, "y": 5 }, | ||
| "fieldConfig": { | ||
| "defaults": { "unit": "short", "decimals": 0, "custom": { "align": "auto", "filterable": true } }, | ||
| "overrides": [ | ||
| { "matcher": { "id": "byName", "options": "Organization" }, "properties": [{ "id": "custom.width", "value": 260 }] } | ||
| ] | ||
| }, | ||
| "options": { "showHeader": true, "footer": { "show": true, "reducer": ["sum"], "fields": "" } }, | ||
| "targets": [ | ||
| { | ||
| "refId": "A", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "editorMode": "code", | ||
| "rawQuery": true, | ||
| "format": "table", | ||
| "rawSql": "SELECT\n COALESCE(o.display_name, o.name) AS \"Organization\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'test_executions'), 0) AS \"Test executions\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'tracing_spans'), 0) AS \"Tracing spans\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'test_generation'), 0) AS \"Tests generated\",\n COALESCE(SUM(u.used) FILTER (WHERE u.resource = 'model_tokens'), 0) AS \"Model tokens\"\nFROM public.usage u\nJOIN public.organization o ON o.id = u.organization_id\nWHERE u.deleted_at IS NULL\n AND o.deleted_at IS NULL\n AND u.period_start = date_trunc('month', now() AT TIME ZONE 'UTC')::date\nGROUP BY 1\nORDER BY 2 DESC, 1" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": 3, | ||
| "type": "bargauge", | ||
| "title": "Top consumers: $resource", | ||
| "description": "Highest ten organizations for the selected resource, current calendar month.", | ||
| "gridPos": { "h": 9, "w": 8, "x": 16, "y": 5 }, | ||
| "fieldConfig": { | ||
| "defaults": { "unit": "short", "decimals": 0, "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } }, | ||
| "overrides": [] | ||
| }, | ||
| "options": { | ||
| "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true }, | ||
| "displayMode": "gradient", | ||
| "orientation": "horizontal", | ||
| "showUnfilled": true | ||
| }, | ||
| "targets": [ | ||
| { | ||
| "refId": "A", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "editorMode": "code", | ||
| "rawQuery": true, | ||
| "format": "table", | ||
| "rawSql": "SELECT\n COALESCE(o.display_name, o.name) AS org,\n SUM(u.used) AS used\nFROM public.usage u\nJOIN public.organization o ON o.id = u.organization_id\nWHERE u.deleted_at IS NULL\n AND o.deleted_at IS NULL\n AND u.resource = '$resource'\n AND u.period_start = date_trunc('month', now() AT TIME ZONE 'UTC')::date\nGROUP BY 1\nHAVING SUM(u.used) > 0\nORDER BY 2 DESC\nLIMIT 10" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "type": "row", | ||
| "title": "Trends", | ||
| "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 }, | ||
| "collapsed": false, | ||
| "panels": [] | ||
| }, | ||
| { | ||
| "id": 4, | ||
| "type": "timeseries", | ||
| "title": "Platform total by month", | ||
| "description": "One point per calendar month, so a time range shorter than a month renders empty. Default range is 12 months.", | ||
| "gridPos": { "h": 9, "w": 12, "x": 0, "y": 15 }, | ||
| "fieldConfig": { | ||
| "defaults": { | ||
| "unit": "short", | ||
| "decimals": 0, | ||
| "custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "barAlignment": 0, "showPoints": "never", "stacking": { "mode": "none" } } | ||
| }, | ||
| "overrides": [] | ||
| }, | ||
| "options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, | ||
| "targets": [ | ||
| { | ||
| "refId": "A", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "editorMode": "code", | ||
| "rawQuery": true, | ||
| "format": "time_series", | ||
| "rawSql": "SELECT\n u.period_start::timestamptz AS \"time\",\n u.resource AS metric,\n SUM(u.used) AS value\nFROM public.usage u\nJOIN public.organization o ON o.id = u.organization_id\nWHERE u.deleted_at IS NULL\n AND o.deleted_at IS NULL\n AND $__timeFilter(u.period_start)\nGROUP BY 1, 2\nORDER BY 1" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": 5, | ||
| "type": "timeseries", | ||
|
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. Improvement Trends panel casts
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. Fixed: Same minor note as above: consider changing |
||
| "title": "$resource by organization", | ||
| "description": "One point per calendar month for the selected resource and organizations.", | ||
| "gridPos": { "h": 9, "w": 12, "x": 12, "y": 15 }, | ||
| "fieldConfig": { | ||
| "defaults": { | ||
| "unit": "short", | ||
| "decimals": 0, | ||
| "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2, "showPoints": "always", "spanNulls": true } | ||
| }, | ||
| "overrides": [] | ||
| }, | ||
| "options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, | ||
| "targets": [ | ||
| { | ||
| "refId": "A", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "editorMode": "code", | ||
| "rawQuery": true, | ||
| "format": "time_series", | ||
| "rawSql": "SELECT\n u.period_start::timestamptz AS \"time\",\n COALESCE(o.display_name, o.name) AS metric,\n SUM(u.used) AS value\nFROM public.usage u\nJOIN public.organization o ON o.id = u.organization_id\nWHERE u.deleted_at IS NULL\n AND o.deleted_at IS NULL\n AND u.resource = '$resource'\n AND COALESCE(o.display_name, o.name) IN (${org:sqlstring})\n AND $__timeFilter(u.period_start)\nGROUP BY 1, 2\nORDER BY 1" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": 6, | ||
| "type": "table", | ||
| "title": "Month over month: $resource", | ||
| "description": "This calendar month against the previous one for the selected resource. Ignores the time picker.", | ||
| "gridPos": { "h": 9, "w": 24, "x": 0, "y": 24 }, | ||
| "fieldConfig": { | ||
| "defaults": { "unit": "short", "decimals": 0, "custom": { "align": "auto", "filterable": true } }, | ||
| "overrides": [ | ||
| { "matcher": { "id": "byName", "options": "Organization" }, "properties": [{ "id": "custom.width", "value": 260 }] }, | ||
|
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. Improvement Org filter/groups use If two orgs share a display name (or a name changes), series/rows can merge and filtering becomes ambiguous.
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. This is addressed now that the org variable is ID-valued and the panels group by |
||
| { "matcher": { "id": "byName", "options": "Change" }, "properties": [{ "id": "custom.cellOptions", "value": { "type": "color-text" } }, { "id": "color", "value": { "mode": "continuous-RdYlGr" } }] } | ||
| ] | ||
| }, | ||
| "options": { "showHeader": true, "sortBy": [{ "displayName": "Change", "desc": true }] }, | ||
| "targets": [ | ||
| { | ||
| "refId": "A", | ||
| "datasource": { "type": "postgres", "uid": "${datasource}" }, | ||
| "editorMode": "code", | ||
| "rawQuery": true, | ||
| "format": "table", | ||
| "rawSql": "WITH bounds AS (\n SELECT\n date_trunc('month', now() AT TIME ZONE 'UTC')::date AS this_month,\n (date_trunc('month', now() AT TIME ZONE 'UTC') - interval '1 month')::date AS prev_month\n),\nmonthly AS (\n SELECT\n COALESCE(o.display_name, o.name) AS org,\n u.period_start,\n SUM(u.used) AS used\n FROM public.usage u\n JOIN public.organization o ON o.id = u.organization_id\n CROSS JOIN bounds b\n WHERE u.deleted_at IS NULL\n AND o.deleted_at IS NULL\n AND u.resource = '$resource'\n AND u.period_start IN (b.prev_month, b.this_month)\n GROUP BY 1, 2\n)\nSELECT\n m.org AS \"Organization\",\n COALESCE(SUM(m.used) FILTER (WHERE m.period_start = b.prev_month), 0) AS \"Previous month\",\n COALESCE(SUM(m.used) FILTER (WHERE m.period_start = b.this_month), 0) AS \"This month\",\n COALESCE(SUM(m.used) FILTER (WHERE m.period_start = b.this_month), 0)\n - COALESCE(SUM(m.used) FILTER (WHERE m.period_start = b.prev_month), 0) AS \"Change\"\nFROM monthly m\nCROSS JOIN bounds b\nGROUP BY 1\nORDER BY 4 DESC" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "type": "row", | ||
| "title": "Notes", | ||
| "gridPos": { "h": 1, "w": 24, "x": 0, "y": 33 }, | ||
| "collapsed": false, | ||
| "panels": [] | ||
| }, | ||
| { | ||
| "id": 7, | ||
| "type": "text", | ||
| "title": "What is and is not here", | ||
| "gridPos": { "h": 5, "w": 24, "x": 0, "y": 34 }, | ||
| "options": { | ||
| "mode": "markdown", | ||
| "content": "**Limits are not shown.** Each tier's quota lives in the EE tier config and the org's signed license, neither of which is readable from SQL. An org's used-vs-limit view is on its own Usage page in the app.\n\n**seats, projects and endpoints are missing on purpose.** They are counted live off the `user`, `project` and `endpoint` tables rather than accrued into `usage`, so they have no history to chart.\n\n**Periods are UTC calendar months.** Each row covers one month, so panels above either pin to the current month or need a range of a month or more." | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Same GSM key as the GRAFANA_VIEWER_PASS entry in external-secrets/rhesis-app-secrets.yaml, | ||
| # synced separately into monitoring so the GrafanaDatasource CR in this namespace can read it. | ||
| apiVersion: external-secrets.io/v1beta1 | ||
| kind: ExternalSecret | ||
| metadata: | ||
| name: grafana-viewer-credentials | ||
| namespace: monitoring | ||
| annotations: | ||
| argocd.argoproj.io/sync-wave: "0" | ||
| spec: | ||
| refreshInterval: 1h | ||
| secretStoreRef: | ||
| name: gcp-secret-manager | ||
| kind: ClusterSecretStore | ||
| target: | ||
| name: grafana-viewer-credentials | ||
| creationPolicy: Owner | ||
| data: | ||
| - secretKey: password | ||
| remoteRef: | ||
| key: dev-grafana-viewer-password |
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.
Improvement
grafana-viewergetsGRANT SELECT ON ALL TABLES IN SCHEMA public+ default privileges for future tables.That’s broader than this dashboard needs (usage + organization) and, combined with
BYPASSRLS, means a Grafana compromise can read any table inpublicacross all tenants.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.
Confirmed this matches the existing stg/prd pattern (CNPG
postInitApplicationSQLalso grantsSELECT ON ALL TABLES IN SCHEMA public+ default privileges).Still worth calling out as a conscious tradeoff:
BYPASSRLS+ full-schema SELECT means a Grafana compromise can read any tenant data inpublic. If scoping isn’t feasible operationally (becauseusageis created later), maybe add an explicit comment in the script explaining why the broader grant is intentional.