diff --git a/.github/workflows/build-and-push.yaml b/.github/workflows/build-and-push.yaml index 3decec17..b69eff30 100644 --- a/.github/workflows/build-and-push.yaml +++ b/.github/workflows/build-and-push.yaml @@ -93,12 +93,12 @@ jobs: uses: ./.github/actions/init-pnpm with: install-filter: '.' - - name: Log in to the Container registry - uses: docker/login-action@v4 - with: - registry: ${{ secrets.ACR_URL }} - username: ${{ secrets.ACR_PUSH_USER }} - password: ${{ secrets.ACR_PUSH_TOKEN }} + - name: Helm | Login + shell: bash + run: echo '${{ secrets.ACR_PUSH_TOKEN }}' | helm registry login -u '${{ secrets.ACR_PUSH_USER }}' --password-stdin '${{ secrets.ACR_URL }}' + env: + HELM_EXPERIMENTAL_OCI: '1' + - name: Get version from manifest id: get_version shell: bash @@ -106,6 +106,9 @@ jobs: VERSION=$(cat .release-please-manifest.json | jq -r '.["."]') echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: build opa + shell: bash + run: helm dependency build ./helm/charts/opa - name: build dependencies shell: bash run: pnpm --filter "./apps/*" -rc --parallel exec 'helm dependency build ./helm || true' diff --git a/.regal/config.yaml b/.regal/config.yaml index b55ace5a..077fe580 100644 --- a/.regal/config.yaml +++ b/.regal/config.yaml @@ -9,9 +9,12 @@ rules: - data.keys.kid - data.users.avi.allowNoBrowser - data.users.avi.allowNoOrigin + - data.users.avi.origins style: line-length: level: ignore + file-length: + level: ignore idiomatic: directory-package-mismatch: level: ignore diff --git a/.vscode/settings.json b/.vscode/settings.json index cdff7df8..13db48e6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,9 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "opa.dependency_paths.opa": "${workspaceFolder}/policy/opa", - "opa.dependency_paths.regal": "${workspaceFolder}/policy/regal" + "opa.dependency_paths.regal": "${workspaceFolder}/policy/regal", + "files.associations": { + "**/helm/dashboards/*.json": "helm", + "**/helm/charts/opa/config/opa.yaml": "helm" + } } diff --git a/helm/Chart.lock b/helm/Chart.lock index b0535667..6f75c9f2 100644 --- a/helm/Chart.lock +++ b/helm/Chart.lock @@ -17,5 +17,8 @@ dependencies: - name: nginx repository: oci://acrarolibotnonprod.azurecr.io/helm/common version: 2.1.6 -digest: sha256:49f1a50c589b1760030c7a449e779d5ca15acdbf8173ef953aceb699b88a170f -generated: "2026-06-01T12:20:36.236385936+03:00" +- name: grafana-datasources + repository: oci://acrarolibotnonprod.azurecr.io/helm/infra + version: 1.0.0 +digest: sha256:0797bfd08fb9ee79413ee32bc2944d1052ed7e250c9140e5a310cbfa4cbf1e73 +generated: "2026-07-22T09:52:31.741378493+03:00" diff --git a/helm/Chart.yaml b/helm/Chart.yaml index f012341f..80a9fe95 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -30,3 +30,7 @@ dependencies: condition: kiosk-ui.enabled alias: kiosk-ui version: 2.1.6 + - name: grafana-datasources + repository: oci://acrarolibotnonprod.azurecr.io/helm/infra + condition: grafana-datasources.enabled + version: 1.0.0 diff --git a/helm/charts/opa/Chart.lock b/helm/charts/opa/Chart.lock index a81c4cb7..018b976e 100644 --- a/helm/charts/opa/Chart.lock +++ b/helm/charts/opa/Chart.lock @@ -2,5 +2,8 @@ dependencies: - name: mclabels repository: oci://acrarolibotnonprod.azurecr.io/helm/infra version: 1.0.1 -digest: sha256:a97237cd8966ab9d4f8c0b8dda2ad110fbff5d485da868124fdce2a5dbbfa208 -generated: "2025-11-20T10:36:07.259160863+02:00" +- name: fluent-bit + repository: oci://ghcr.io/fluent/helm-charts + version: 0.57.7 +digest: sha256:d4c09206ec7d0bee95f3cae64e127bfe93c4f3d827cdc1aac2a3e695210716b8 +generated: "2026-06-17T11:32:28.877870547+03:00" diff --git a/helm/charts/opa/Chart.yaml b/helm/charts/opa/Chart.yaml index aeb38d62..f1e52f1d 100644 --- a/helm/charts/opa/Chart.yaml +++ b/helm/charts/opa/Chart.yaml @@ -8,3 +8,8 @@ dependencies: - name: mclabels version: 1.0.1 repository: oci://acrarolibotnonprod.azurecr.io/helm/infra + - name: fluent-bit + repository: oci://ghcr.io/fluent/helm-charts + version: 0.57.7 + condition: fluent-bit.enabled + alias: fluentbit diff --git a/helm/charts/opa/config/opa.yaml b/helm/charts/opa/config/opa.yaml index 256959af..525685cb 100644 --- a/helm/charts/opa/config/opa.yaml +++ b/helm/charts/opa/config/opa.yaml @@ -5,6 +5,10 @@ services: s3_signing: # Required for finding the envs for S3 environment_credentials: {} +{{- if .Values.fluentbit.enabled }} + fluent-bit: + url: {{ .Values.fluentbit.url | default (printf "http://%s-fluentbit:9880" .Release.Name)}}/opa.logs +{{- end }} bundles: authz: @@ -21,8 +25,15 @@ status: decision_logs: console: {{ .Values.decisionLogs.console }} + {{- if .Values.fluentbit.enabled }} + service: fluent-bit + {{- end }} reporting: - buffer_size_limit_bytes: {{ .Values.decisionLogs.maxBufferSize }} + buffer_type: event + buffer_size_limit_events: {{ .Values.decisionLogs.maxBufferEvents }} + upload_size_limit_bytes: {{ .Values.decisionLogs.maxUploadSizeBytes }} + min_delay_seconds: {{ .Values.decisionLogs.minDelaySeconds }} + max_delay_seconds: {{ .Values.decisionLogs.maxDelaySeconds }} {{ if .Values.tracing.enabled }} distributed_tracing: @@ -38,8 +49,8 @@ storage: auto_create: true directory: {{ .Values.bundlePersistencePath }} - labels: + environment: {{ .Values.opaEnvironment | required "opaEnvironment is required" | quote }} {{- range $key, $value := .Values.labels }} {{ $key }}: {{ $value | quote }} {{- end }} diff --git a/helm/charts/opa/templates/deployment.yaml b/helm/charts/opa/templates/deployment.yaml index 998bcbdb..d553203e 100644 --- a/helm/charts/opa/templates/deployment.yaml +++ b/helm/charts/opa/templates/deployment.yaml @@ -36,9 +36,7 @@ spec: {{- end }} annotations: {{ include "mclabels.annotations" . | nindent 8 }} - {{- if .Values.resetOnConfigChange }} checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- end }} {{- if .Values.additionalPodAnnotations }} {{- toYaml .Values.additionalPodAnnotations | nindent 8}} {{- end }} diff --git a/helm/charts/opa/values.yaml b/helm/charts/opa/values.yaml index 1fa98740..306690c7 100644 --- a/helm/charts/opa/values.yaml +++ b/helm/charts/opa/values.yaml @@ -59,7 +59,10 @@ status: decisionLogs: console: true - maxBufferSize: 5242880 # 5MB + minDelaySeconds: 5 + maxDelaySeconds: 60 + maxBufferEvents: 30000 + maxUploadSizeBytes: 1048576 tracing: enabled: false @@ -135,3 +138,199 @@ ingress: # Add any label you want as `KEY: VALUE` labels: {} + +fluentbit: + enabled: true + kind: Deployment + testFramework: + enabled: false + imagePullSecrets: + - name: my-registry-secret + serviceAccount: + create: false + rbac: + create: false + openshift: + enabled: true + resources: + limits: + cpu: 400m + memory: 256Mi + requests: + cpu: 100m + memory: 256Mi + autoscaling: + enabled: true + labels: + mapcolonies.io/environment: dev + mapcolonies.io/component: "infrastructure" + mapcolonies.io/part-of: "monitoring" + mapcolonies.io/owner: "infra" + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "2021" + prometheus.io/path: "/metrics" + loki: + url: loki.namespace.svc.cluster.local + port: "3100" + envWithTpl: + - name: FLUENT_LOKI_HOST + value: "{{ .Values.loki.url }}" + - name: FLUENT_LOKI_PORT + value: "{{ .Values.loki.port }}" + extraPorts: + - name: input + port: 9880 + containerPort: 9880 + protocol: TCP + - name: prom-metrics + port: 2021 + containerPort: 2021 + protocol: TCP + luaScripts: + transform.lua: | + function transform_opa_log(tag, timestamp, record) + local new_record = {} + -- 1. Input and Bundles + new_record["input"] = record["input"] + new_record["bundles"] = record["bundles"] + + -- 2. Trace & Span IDs + new_record["trace_id"] = record["trace_id"] + new_record["span_id"] = record["span_id"] + + -- 3. Extract Environment and Domain + if record["labels"] ~= nil then + new_record["env"] = record["labels"]["environment"] + end + + new_record["opa_domain"] = record["input"]["domain"] + + new_record["token_location"] = "missing" + if record["input"]["headers"] ~= nil and record["input"]["headers"]["x-api-key"] ~= nil then + new_record["token_location"] = "header" + end + + if record["input"]["query"] ~= nil and record["input"]["query"]["token"] ~= nil then + if new_record["token_location"] == "header" then + new_record["token_location"] = "both" + else + new_record["token_location"] = "query" + end + end + + for bundle_name, bundle_data in pairs(record["bundles"] or {}) do + if bundle_data.revision then + new_record["bundle_revision"] = bundle_data["revision"] + end + end + + if record["input"]["headers"] ~= nil then + new_record["origin"] = record["input"]["headers"]["origin"] + new_record["user_agent"] = record["input"]["headers"]["user-agent"] + new_record["referer"] = record["input"]["headers"]["referer"] + end + + -- 4. Requested By + new_record["requested_by"] = record["requested_by"] + + -- 5. Result splits + if record["result"] ~= nil then + -- Loki metadata requires strings, so we cast the boolean + new_record["allowed"] = tostring(record["result"]["allowed"]) + new_record["reasons"] = record["result"]["reasons"] + new_record["codes"] = record["result"]["codes"] + new_record["sub"] = record["result"]["metadata"]["sub"] + new_record["kid"] = record["result"]["metadata"]["kid"] + new_record["warn"] = record["result"]["warnings"] + end + + -- 6. The original OPA timestamp string + new_record["opa_timestamp"] = record["timestamp"] + + -- 7. Total processing time + if record["metrics"] ~= nil then + new_record["total_time_ns"] = tostring(record["metrics"]["timer_server_handler_ns"]) + end + + -- We return the Fluent Bit ingestion timestamp as the official log time. + return 1, timestamp, new_record + end + + config: + inputs: | + [INPUT] + Name http + Listen 0.0.0.0 + Port 9880 + Tag opa.logs + + [INPUT] + Name fluentbit_metrics + Tag internal.metrics + Scrape_Interval 30 + filters: | + [FILTER] + Name log_to_metrics + Match opa.logs* + Tag opa.metrics + Metric_Name opa_decisions_total + Metric_Mode counter + Metric_Description Total OPA decisions by status, subject, and domain + Add_Label status $result['computed_status'] + Add_Label subject $result['metadata']['sub'] + Add_Label domain $input['domain'] + Add_Label opa_env $labels['environment'] + + [FILTER] + Name log_to_metrics + Match opa.logs* + Tag opa.metrics + Metric_Name opa_server_handler_ns + Metric_Mode histogram + Metric_Description OPA server handler latency in nanoseconds - not full request latency, but the time spent in the OPA handler itself + Value_Field $metrics['timer_server_handler_ns'] + Add_Label status $result['computed_status'] + Add_Label opa_env $labels['environment'] + Bucket 50000 + Bucket 100000 + Bucket 250000 + Bucket 500000 + Bucket 1000000 + Bucket 2500000 + Bucket 5000000 + Bucket 10000000 + Bucket 25000000 + Bucket 50000000 + + [FILTER] + Name grep + Match opa.logs* + Exclude $result['computed_status'] ^allowed$ + + [FILTER] + Name lua + Match opa.logs* + script /fluent-bit/scripts/transform.lua + call transform_opa_log + outputs: | + [OUTPUT] + Name loki + Match opa.logs* + Host ${FLUENT_LOKI_HOST} + Port ${FLUENT_LOKI_PORT} + + # LABELS: Domain added here for fast stream routing + Labels job=opa, environment=$env, opa_domain=$opa_domain + + # METADATA: Allowed moved here, along with other high-cardinality data + Structured_metadata trace_id=$trace_id, span_id=$span_id, requested_by=$requested_by, reason=$reason, subject=$sub, token_kid=$kid, warnings=$warnings, total_time_ns=$total_time_ns, allowed=$allowed, token_location=$token_location, origin=$origin, user_agent=$user_agent, bundle_revision=$bundle_revision, origin=$origin, referer=$referer + + # CLEANUP: Strip these fields from the main JSON log body + Remove_Keys env, domain, allowed, trace_id, span_id, requested_by, sub, total_time_ns, opa_domain, token_location, origin, user_agent, bundles, referer + + [OUTPUT] + Name prometheus_exporter + Match * + Host 0.0.0.0 + Port 2021 diff --git a/helm/dashboards/analytics.json b/helm/dashboards/analytics.json new file mode 100644 index 00000000..0ddc3836 --- /dev/null +++ b/helm/dashboards/analytics.json @@ -0,0 +1,1183 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 324, + "links": [ + { + "asDropdown": false, + "icon": "dashboard", + "includeVars": false, + "keepTime": true, + "tags": [], + "targetBlank": false, + "title": "Decision logs (raw records)", + "type": "link", + "url": "/d/opala-decision-logs/decision-logs?var-environment=${env:text}&var-subject=$__all&var-domains=$__all&var-error_codes=$__all" + } + ], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "**Denial Analytics** — aggregated statistics for denied and warned decisions: who is failing, how much, and with which error codes. The filters above apply to every panel; the **Error code** filter only affects the code panels (the Prometheus counter carries no code label). Subjects and codes in the tables link to [Decision Logs](/d/opala-decision-logs/decision-logs) with the matching filters and time range applied. Panels are empty for environments with no decision-log traffic.", + "mode": "markdown" + }, + "pluginVersion": "12.3.1", + "title": "", + "transparent": true, + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 10, + "panels": [], + "title": "Landscape", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Rate of non-allowed decisions (denied, warning) for the selected filters, from the decision-log metric pipeline. Empty means no denials or warnings are flowing for this selection.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "denied" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "warning" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 0, + "y": 4 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum by (status) (rate(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status!=\"allowed\"}[$__rate_interval]))", + "legendFormat": "{{ "{{" }}status{{ "}}" }}", + "refId": "A" + } + ], + "title": "Failure rate by status", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "description": "Denied decisions per error code over time — exact counts from Loki (which receives only non-allowed decisions). A denial carrying several codes shows as a comma-joined category. A new color appearing here is a new failure mode.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 10, + "y": 4 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "sum by (code) (count_over_time({job=\"opa\", environment=\"${env:text}\", opa_domain=~\"${domain:regex}\"} | subject=~\"${subject:regex}\" |~ `${code:regex}` | allowed=\"false\" | json codes | label_format code=`{{ "{{" }} .codes | replace \"\\\"\" \"\" | replace \"[\" \"\" | replace \"]\" \"\" {{ "}}" }}` [$__auto]))", + "legendFormat": "{{ "{{" }}code{{ "}}" }}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Denials by code", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Share of all decisions that were denied in the dashboard time range, for the selected filters. No data means no decisions at all for this selection.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 0.05 + }, + { + "color": "red", + "value": 0.25 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 20, + "y": 4 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum(increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status=\"denied\"}[$__range])) / sum(increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\"}[$__range]))", + "instant": true, + "refId": "A" + } + ], + "title": "Denial ratio", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 20, + "panels": [], + "title": "Who's in trouble", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Subjects ranked by the share of their requests that were denied in the time range. Subjects with fewer than 10 requests are excluded from the ratio so one-off probes don't pin the ranking. Click a subject to open its raw records in Decision logs. Counts are counter-based: a brand-new subject whose entire burst fits inside one scrape interval can undercount here — the code panels below count exactly from Loki.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Denial ratio" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 1 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 0.05 + }, + { + "color": "red", + "value": 0.25 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "subject" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open raw records in Decision logs", + "url": "/d/opala-decision-logs/decision-logs?var-environment=${env:text}&var-subject=${__data.fields.subject}&var-domains=$__all&var-error_codes=$__all&${__url_time_range}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 21, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Denial ratio" + } + ] + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\"}[$__range]))", + "format": "table", + "instant": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status=\"denied\"}[$__range]))", + "format": "table", + "instant": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status=\"warning\"}[$__range]))", + "format": "table", + "instant": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "(sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status=\"denied\"}[$__range])) / (sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\"}[$__range])) > $min_requests)) or ((sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\"}[$__range])) > $min_requests) * 0)", + "format": "table", + "instant": true, + "refId": "D" + } + ], + "title": "Subjects by denial ratio", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Value #A": 2, + "Value #B": 3, + "Value #C": 4, + "Value #D": 1, + "subject": 0 + }, + "renameByName": { + "Value #A": "Requests", + "Value #B": "Denied", + "Value #C": "Warnings", + "Value #D": "Denial ratio" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Most-denied subjects by absolute count in the time range — the impact view. Click a bar to open raw records.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "decimals": 0, + "links": [ + { + "targetBlank": true, + "title": "Open raw records in Decision logs", + "url": "/d/opala-decision-logs/decision-logs?var-environment=${env:text}&var-subject=${__field.labels.subject}&var-domains=$__all&var-error_codes=$__all&${__url_time_range}" + } + ], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 6, + "x": 12, + "y": 13 + }, + "id": 22, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "topk(10, sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status=\"denied\"}[$__range])) > 0)", + "instant": true, + "legendFormat": "{{ "{{" }}subject{{ "}}" }}", + "refId": "A" + } + ], + "title": "Top denied subjects", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Subjects accumulating warnings in the time range — today's warnings are often next week's denials (e.g. expiring credentials). Click a bar to open raw records.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "orange", + "mode": "fixed" + }, + "decimals": 0, + "links": [ + { + "targetBlank": true, + "title": "Open raw records in Decision logs", + "url": "/d/opala-decision-logs/decision-logs?var-environment=${env:text}&var-subject=${__field.labels.subject}&var-domains=$__all&var-error_codes=$__all&${__url_time_range}" + } + ], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 23, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "topk(10, sum by (subject) (increase(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\", subject=~\"${subject:regex}\", domain=~\"${domain:regex}\", status=\"warning\"}[$__range])) > 0)", + "instant": true, + "legendFormat": "{{ "{{" }}subject{{ "}}" }}", + "refId": "A" + } + ], + "title": "Top warned subjects", + "type": "bargauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 30, + "panels": [], + "title": "Why — error codes", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "description": "Exact denial counts per error code for the time range, from Loki. Multi-code denials show as comma-joined categories.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "displayName": "${__field.labels.code}", + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 24 + }, + "id": 31, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [ + "value" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "sum by (code) (count_over_time({job=\"opa\", environment=\"${env:text}\", opa_domain=~\"${domain:regex}\"} | subject=~\"${subject:regex}\" |~ `${code:regex}` | allowed=\"false\" | json codes | label_format code=`{{ "{{" }} .codes | replace \"\\\"\" \"\" | replace \"[\" \"\" | replace \"]\" \"\" {{ "}}" }}` [$__range]))", + "legendFormat": "{{ "{{" }}code{{ "}}" }}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Denials by code (time range total)", + "type": "piechart" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "description": "Which subject is failing with which error code — exact counts from Loki for the time range. Click a subject or code to open the matching raw records in Decision logs.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "decimals": 0, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "subject" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open raw records in Decision logs", + "url": "/d/opala-decision-logs/decision-logs?var-environment=${env:text}&var-subject=${__data.fields.subject}&var-error_codes=${__data.fields.code}&var-domains=$__all&${__url_time_range}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "code" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open raw records in Decision logs", + "url": "/d/opala-decision-logs/decision-logs?var-environment=${env:text}&var-subject=${__data.fields.subject}&var-error_codes=${__data.fields.code}&var-domains=$__all&${__url_time_range}" + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 16, + "x": 8, + "y": 24 + }, + "id": 32, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Denials" + } + ] + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "sum by (code, subject) (count_over_time({job=\"opa\", environment=\"${env:text}\", opa_domain=~\"${domain:regex}\"} | subject=~\"${subject:regex}\" |~ `${code:regex}` | allowed=\"false\" | json codes | label_format code=`{{ "{{" }} .codes | replace \"\\\"\" \"\" | replace \"[\" \"\" | replace \"]\" \"\" {{ "}}" }}` [$__range]))", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Code × subject", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Value #A": 2, + "code": 1, + "subject": 0 + }, + "renameByName": { + "Value": "Denials", + "Value #A": "Denials" + } + } + } + ], + "type": "table" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 42, + "tags": [ + "OPAla" + ], + "templating": { + "list": [ + { + "current": { + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 2, + "label": "Prometheus", + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "/^Prometheus$/", + "type": "datasource" + }, + { + "current": { + "text": "Loki", + "value": "loki" + }, + "hide": 2, + "label": "Loki", + "name": "loki", + "options": [], + "query": "loki", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "np", + "value": "np" + }, + "includeAll": false, + "label": "Environment", + "name": "env", + "options": [ + { + "selected": true, + "text": "development", + "value": "np" + }, + { + "selected": false, + "text": "staging", + "value": "stage" + }, + { + "selected": false, + "text": "production", + "value": "prod" + } + ], + "query": "development : np,staging : stage,production : prod", + "type": "custom" + }, + { + "allValue": ".*", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "definition": "label_values(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}, domain)", + "includeAll": true, + "label": "Domain", + "multi": true, + "name": "domain", + "options": [], + "query": "label_values(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}, domain)", + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "definition": "label_values(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}, subject)", + "includeAll": true, + "label": "Subject", + "multi": true, + "name": "subject", + "options": [], + "query": "label_values(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}, subject)", + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "text": [ + "$__all" + ], + "value": [ + "$__all" + ] + }, + "includeAll": true, + "label": "Error code", + "multi": true, + "name": "code", + "options": [ + { + "selected": false, + "text": "token_not_valid", + "value": "token_not_valid" + }, + { + "selected": false, + "text": "no_token_supplied", + "value": "no_token_supplied" + }, + { + "selected": false, + "text": "user_not_found", + "value": "user_not_found" + }, + { + "selected": false, + "text": "domain_missing", + "value": "domain_missing" + }, + { + "selected": false, + "text": "domain_check_failed", + "value": "domain_check_failed" + }, + { + "selected": false, + "text": "user_agent_missing", + "value": "user_agent_missing" + }, + { + "selected": false, + "text": "user_agent_not_allowed", + "value": "user_agent_not_allowed" + }, + { + "selected": false, + "text": "c2b_ua_not_allowed", + "value": "c2b_ua_not_allowed" + }, + { + "selected": false, + "text": "bot_user_agent_detected", + "value": "bot_user_agent_detected" + }, + { + "selected": false, + "text": "no_origins_configured", + "value": "no_origins_configured" + }, + { + "selected": false, + "text": "ie11_missing_origin_and_referer", + "value": "ie11_missing_origin_and_referer" + }, + { + "selected": false, + "text": "ie11_origin_and_referer_check_failed", + "value": "ie11_origin_and_referer_check_failed" + }, + { + "selected": false, + "text": "api_missing_origin_and_referer", + "value": "api_missing_origin_and_referer" + }, + { + "selected": false, + "text": "api_origin_mismatch", + "value": "api_origin_mismatch" + }, + { + "selected": false, + "text": "api_request_referer_check_failed", + "value": "api_request_referer_check_failed" + }, + { + "selected": false, + "text": "unknown_error", + "value": "unknown_error" + } + ], + "query": "token_not_valid,no_token_supplied,user_not_found,domain_missing,domain_check_failed,user_agent_missing,user_agent_not_allowed,c2b_ua_not_allowed,bot_user_agent_detected,no_origins_configured,ie11_missing_origin_and_referer,ie11_origin_and_referer_check_failed,api_missing_origin_and_referer,api_origin_mismatch,api_request_referer_check_failed,unknown_error", + "type": "custom" + }, + { + "current": { + "text": "10", + "value": "10" + }, + "hide": 2, + "label": "Min requests for ratio ranking", + "name": "min_requests", + "query": "10", + "skipUrlSync": true, + "type": "constant" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Denial Analytics", + "uid": "opala-denial-analytics", + "version": 3 +} diff --git a/helm/dashboards/bundle.json b/helm/dashboards/bundle.json new file mode 100644 index 00000000..3021f94c --- /dev/null +++ b/helm/dashboards/bundle.json @@ -0,0 +1,1025 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 301, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Tracks whether OPA pods are running the bundle Postgres built for them, per environment.\n\n*Table* — OPA pods running in the namespace that serves the selected environment. **Health**: 🟢 on the latest revision and polling; 🟡 a newer revision was just published and the pod hasn't caught up yet (within the grace window); 🔴 wrong revision past the grace window, or not polling.", + "mode": "markdown" + }, + "pluginVersion": "12.3.1", + "title": "", + "type": "text" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 5, + "x": 8, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "format": "table", + "rawSql": "select revision from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refId": "A" + } + ], + "title": "Latest DB revision ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dateTimeAsIso" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 13, + "y": 0 + }, + "id": 7, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "format": "table", + "rawSql": "select created_at from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refId": "A" + } + ], + "title": "DB bundle created", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 5, + "x": 19, + "y": 0 + }, + "id": 8, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "format": "table", + "rawSql": "select opa_version from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refId": "A" + } + ], + "title": "Built for OPA version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "One row per pod whose active_revision belongs to $env, one column per metric. Health is red if the pod's active_revision doesn't match the current DB revision, or if it hasn't polled in >180s.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Last activation (s)" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last download (s)" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last poll (s)" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 180 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Health" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "color": "yellow", + "text": "⏳ Catching up" + }, + "2": { + "color": "red", + "text": "⚠ Problem" + } + }, + "type": "value" + }, + { + "options": { + "match": "null", + "result": { + "color": "green", + "text": "✅ OK" + } + }, + "type": "special" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pod" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open pod in OpenShift", + "url": "${console_url}/k8s/ns/${__data.fields.Namespace}/pods/${__data.fields.Pod}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Release" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open deployment in OpenShift", + "url": "${console_url}/k8s/ns/${__data.fields.Namespace}/deployments/${__data.fields.Release}-opa" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 3, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (instance, namespace, release, app_kubernetes_io_version, active_revision) (time() - (last_success_bundle_activation{namespace=~\"$ns\"} / 1e9))", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (instance) (time() - (last_bundle_request{namespace=~\"$ns\"} / 1e9))", + "format": "table", + "instant": true, + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (instance) (time() - (last_success_bundle_download{namespace=~\"$ns\"} / 1e9))", + "format": "table", + "instant": true, + "range": false, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "(((max by (instance) (((time() - last_bundle_request/1e9) > ($grace_window_minutes * 60)) and on(instance) last_success_bundle_activation{namespace=~\"$ns\"})) * 0 + 2) or ((max by (instance) (last_success_bundle_activation{namespace=~\"$ns\"} unless on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\"))) * 0 + (1 + $grace_expired)))", + "format": "table", + "instant": true, + "range": false, + "refId": "D" + } + ], + "title": "OPA pods matching $env", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "renameByName": { + "Value #A": "Last activation (s)", + "Value #B": "Last poll (s)", + "Value #C": "Last download (s)", + "Value #D": "Health", + "active_revision": "Active Revision", + "app_kubernetes_io_version": "OPA Version", + "instance": "Pod", + "namespace": "Namespace", + "release": "Release" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Does any pod whose active_revision belongs to $env report the exact revision that's currently latest in Postgres for $env?", + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "from": 1, + "result": { + "color": "green", + "text": "MATCH" + }, + "to": 999 + }, + "type": "range" + } + ], + "noValue": "MISMATCH", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 5, + "x": 0, + "y": 13 + }, + "id": 4, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "count" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "last_success_bundle_activation{namespace=~\"$ns\"} and on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\")", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "Revision match ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Seconds since the most-recently-polling pod whose active_revision belongs to $env last asked for a bundle (last_bundle_request — the only metric confirmed to update on every poll).", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 180 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 5, + "x": 5, + "y": 13 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "min(time() - (last_bundle_request{namespace=~\"$ns\"} / 1e9))", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "Pod poll liveness ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "description": "Informational only — age alone isn't a problem; a bundle can be hours old and perfectly fine if the pod already activated it. Pair with Revision match / Pod health for actual status.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + }, + "unit": "dtdurations" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 5, + "x": 10, + "y": 13 + }, + "id": 5, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "format": "table", + "rawSql": "select extract(epoch from (now() - created_at))::int as age_seconds from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refId": "A" + } + ], + "title": "DB bundle freshness ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Healthy" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Catching up" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Problem" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 15, + "y": 13 + }, + "id": 9, + "options": { + "displayLabels": [ + "value", + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "(count(last_success_bundle_activation{namespace=~\"$ns\"}) or vector(0)) - (count((((max by (instance) (((time() - last_bundle_request/1e9) > ($grace_window_minutes * 60)) and on(instance) last_success_bundle_activation{namespace=~\"$ns\"})) * 0 + 2) or ((max by (instance) (last_success_bundle_activation{namespace=~\"$ns\"} unless on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\"))) * 0 + (1 + $grace_expired))) == 1) or vector(0)) - (count((((max by (instance) (((time() - last_bundle_request/1e9) > ($grace_window_minutes * 60)) and on(instance) last_success_bundle_activation{namespace=~\"$ns\"})) * 0 + 2) or ((max by (instance) (last_success_bundle_activation{namespace=~\"$ns\"} unless on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\"))) * 0 + (1 + $grace_expired))) == 2) or vector(0))", + "instant": true, + "legendFormat": "Healthy", + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "count((((max by (instance) (((time() - last_bundle_request/1e9) > ($grace_window_minutes * 60)) and on(instance) last_success_bundle_activation{namespace=~\"$ns\"})) * 0 + 2) or ((max by (instance) (last_success_bundle_activation{namespace=~\"$ns\"} unless on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\"))) * 0 + (1 + $grace_expired))) == 1) or vector(0)", + "instant": true, + "legendFormat": "Catching up", + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "count((((max by (instance) (((time() - last_bundle_request/1e9) > ($grace_window_minutes * 60)) and on(instance) last_success_bundle_activation{namespace=~\"$ns\"})) * 0 + 2) or ((max by (instance) (last_success_bundle_activation{namespace=~\"$ns\"} unless on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\"))) * 0 + (1 + $grace_expired))) == 2) or vector(0)", + "instant": true, + "legendFormat": "Problem", + "range": false, + "refId": "C" + } + ], + "title": "Pod health ($env) — how many pods are up to date and are polling normally", + "type": "piechart" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 42, + "tags": [ + "OPAla" + ], + "templating": { + "list": [ + { + "current": { + "text": "infra-opala-{{ include "opa-la.environment" . }}", + "value": "infra-opala-{{ include "opa-la.environment" . }}" + }, + "hide": 2, + "label": "Postgres", + "name": "pg", + "options": [], + "query": "grafana-postgresql-datasource", + "refresh": 1, + "regex": "/.*opala.*/", + "type": "datasource" + }, + { + "current": { + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 2, + "label": "Prometheus", + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "/^Prometheus$/", + "type": "datasource" + }, + { + "current": { + "text": "np", + "value": "np" + }, + "includeAll": false, + "label": "Environment", + "name": "env", + "options": [ + { + "selected": true, + "text": "development", + "value": "np" + }, + { + "selected": false, + "text": "staging", + "value": "stage" + }, + { + "selected": false, + "text": "production", + "value": "prod" + } + ], + "query": "development : np,staging : stage,production : prod", + "type": "custom" + }, + { + "current": { + "text": "np-e9ed4a8eb683", + "value": "np-e9ed4a8eb683" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select revision from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "hide": 2, + "label": "Latest DB revision", + "name": "latest_revision", + "options": [], + "query": "select revision from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refresh": 1, + "skipUrlSync": true, + "type": "query" + }, + { + "current": { + "text": "5", + "value": "5" + }, + "hide": 2, + "label": "Grace window (minutes)", + "name": "grace_window_minutes", + "query": "5", + "skipUrlSync": true, + "type": "constant" + }, + { + "current": { + "text": "{{ .Values.dashboards.values.prod.openshiftUrl }}", + "value": "{{ .Values.dashboards.values.prod.openshiftUrl }}" + }, + "hide": 2, + "includeAll": false, + "label": "OpenShift console", + "name": "console_url", + "options": [ + { + "selected": true, + "text": "np", + "value": "{{ .Values.dashboards.values.np.openshiftUrl }}" + }, + { + "selected": true, + "text": "stage", + "value": "{{ .Values.dashboards.values.stage.openshiftUrl }}" + }, + { + "selected": true, + "text": "prod", + "value": "{{ .Values.dashboards.values.prod.openshiftUrl }}" + } + ], + "query": "np : {{ .Values.dashboards.values.np.openshiftUrl }},stage : {{ .Values.dashboards.values.stage.openshiftUrl }},prod : {{ .Values.dashboards.values.prod.openshiftUrl }}", + "skipUrlSync": true, + "type": "custom" + }, + { + "current": { + "text": "{{ .Values.dashboards.values.prod.namespace }}", + "value": "{{ .Values.dashboards.values.prod.namespace }}" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select (case '$env' when 'np' then '{{ .Values.dashboards.values.np.namespace }}' when 'stage' then '{{ .Values.dashboards.values.stage.namespace }}' when 'prod' then '{{ .Values.dashboards.values.prod.namespace }}' end) as ns", + "hide": 2, + "includeAll": false, + "label": "Namespace (mapped from env)", + "name": "ns", + "options": [], + "query": "select (case '$env' when 'np' then '{{ .Values.dashboards.values.np.namespace }}' when 'stage' then '{{ .Values.dashboards.values.stage.namespace }}' when 'prod' then '{{ .Values.dashboards.values.prod.namespace }}' end) as ns", + "refresh": 1, + "skipUrlSync": true, + "type": "query" + }, + { + "current": { + "text": "1", + "value": "1" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select coalesce((select case when extract(epoch from (now() - created_at)) > ($grace_window_minutes * 60) then 1 else 0 end from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1), 1)", + "hide": 2, + "includeAll": false, + "label": "Grace expired", + "name": "grace_expired", + "options": [], + "query": "select coalesce((select case when extract(epoch from (now() - created_at)) > ($grace_window_minutes * 60) then 1 else 0 end from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1), 1)", + "refresh": 1, + "skipUrlSync": true, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Bundle Status", + "uid": "opala-bundle-status", + "version": 20 +} diff --git a/helm/dashboards/decision.json b/helm/dashboards/decision.json new file mode 100644 index 00000000..9182489e --- /dev/null +++ b/helm/dashboards/decision.json @@ -0,0 +1,615 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-text" + }, + "footer": { + "reducers": [] + }, + "hideFrom": { + "viz": false + }, + "inspect": false + }, + "mappings": [ + { + "options": { + "N/A": { + "color": "red", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "total_time_ns" + }, + "properties": [ + { + "id": "unit", + "value": "ns" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "opa_timestamp" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsIso" + }, + { + "id": "custom.width", + "value": 160 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "allowed" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "false": { + "color": "red", + "index": 1, + "text": "false 😡" + }, + "true": { + "color": "yellow", + "index": 0, + "text": "warn 😳" + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "reasons" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "pill" + } + }, + { + "id": "custom.width", + "value": 314 + }, + { + "id": "custom.tooltip.field", + "value": "codes" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "match": "null", + "result": { + "index": 0, + "text": "😌" + } + }, + "type": "special" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "codes" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "pill" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "warnings" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "pill" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "input" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "json-view" + } + }, + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "token_location" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "both": { + "color": "orange", + "index": 3 + }, + "header": { + "color": "green", + "index": 2 + }, + "missing": { + "color": "red", + "index": 1 + }, + "query": { + "color": "green", + "index": 0 + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "codes" + }, + "properties": [ + { + "id": "custom.hideFrom.viz", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 20, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "lg", + "enablePagination": true, + "frozenColumns": { + "left": 2 + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "opa_timestamp" + } + ] + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "direction": "backward", + "editorMode": "builder", + "expr": "{job=\"opa\", environment=\"$environment\"} |~ `$error_codes` | subject =~ `$subject` | opa_domain =~ `$domains` | json opa_timestamp, reasons, input, warn, codes", + "queryType": "range", + "refId": "A" + } + ], + "title": "", + "transformations": [ + { + "id": "extractFields", + "options": { + "delimiter": ",", + "format": "json", + "keepTime": false, + "replace": false, + "source": "labels" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Line": true, + "Time": true, + "codes": false, + "detected_level": true, + "id": true, + "job": true, + "labelTypes": true, + "labels": true, + "service_name": true, + "span_id": true, + "total_time_ns": false, + "trace_id": true, + "tsNs": true + }, + "includeByName": {}, + "indexByName": { + "Line": 23, + "Time": 3, + "allowed": 1, + "bundle_revision": 19, + "codes": 25, + "detected_level": 8, + "environment": 9, + "id": 7, + "input": 24, + "job": 10, + "labelTypes": 6, + "labels": 4, + "opa_domain": 11, + "opa_timestamp": 0, + "origin": 17, + "reasons": 12, + "referer": 18, + "service_name": 20, + "span_id": 21, + "subject": 13, + "token_kid": 14, + "token_location": 16, + "total_time_ns": 2, + "trace_id": 22, + "tsNs": 5, + "warn": 15 + }, + "renameByName": { + "total_time_ns": "Duration", + "warn": "warnings" + } + } + }, + { + "id": "convertFieldType", + "options": { + "conversions": [ + { + "destinationType": "other", + "targetField": "reasons" + } + ], + "fields": {} + } + } + ], + "type": "table" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "allowCustomValue": false, + "current": { + "text": "infra-opala-{{ include "opa-la.environment" . }}", + "value": "infra-opala-{{ include "opa-la.environment" . }}" + }, + "hide": 2, + "name": "pg", + "options": [], + "query": "grafana-postgresql-datasource", + "refresh": 1, + "regex": "/.*opala.*/", + "type": "datasource" + }, + { + "allowCustomValue": false, + "current": { + "text": "Loki", + "value": "loki" + }, + "hide": 2, + "name": "loki", + "options": [], + "query": "loki", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allowCustomValue": false, + "current": { + "text": "development", + "value": "development" + }, + "label": "Environment", + "name": "environment", + "options": [ + { + "selected": true, + "text": "development", + "value": "development" + }, + { + "selected": false, + "text": "staging", + "value": "staging" + }, + { + "selected": false, + "text": "production", + "value": "production" + } + ], + "query": "development,staging,production", + "type": "custom" + }, + { + "allValue": ".*", + "allowCustomValue": false, + "current": { + "text": [ + "token_not_valid" + ], + "value": [ + "token_not_valid" + ] + }, + "description": "", + "includeAll": true, + "label": "Error codes", + "multi": true, + "name": "error_codes", + "options": [ + { + "selected": true, + "text": "token_not_valid", + "value": "token_not_valid" + }, + { + "selected": false, + "text": "no_token_supplied", + "value": "no_token_supplied" + }, + { + "selected": false, + "text": "user_not_found", + "value": "user_not_found" + }, + { + "selected": false, + "text": "domain_missing", + "value": "domain_missing" + }, + { + "selected": false, + "text": "domain_check_failed", + "value": "domain_check_failed" + }, + { + "selected": false, + "text": "user_agent_missing", + "value": "user_agent_missing" + }, + { + "selected": false, + "text": "user_agent_not_allowed", + "value": "user_agent_not_allowed" + }, + { + "selected": false, + "text": "c2b_ua_not_allowed", + "value": "c2b_ua_not_allowed" + }, + { + "selected": false, + "text": "bot_user_agent_detected", + "value": "bot_user_agent_detected" + }, + { + "selected": false, + "text": "no_origins_configured", + "value": "no_origins_configured" + }, + { + "selected": false, + "text": "ie11_missing_origin_and_referer", + "value": "ie11_missing_origin_and_referer" + }, + { + "selected": false, + "text": "ie11_origin_and_referer_check_failed", + "value": "ie11_origin_and_referer_check_failed" + }, + { + "selected": false, + "text": "api_missing_origin_and_referer", + "value": "api_missing_origin_and_referer" + }, + { + "selected": false, + "text": "api_origin_mismatch", + "value": "api_origin_mismatch" + }, + { + "selected": false, + "text": "api_request_referer_check_failed", + "value": "api_request_referer_check_failed" + }, + { + "selected": false, + "text": "unknown_error", + "value": "unknown_error" + } + ], + "query": "token_not_valid,no_token_supplied,user_not_found,domain_missing,domain_check_failed,user_agent_missing,user_agent_not_allowed,c2b_ua_not_allowed,bot_user_agent_detected,no_origins_configured,ie11_missing_origin_and_referer,ie11_origin_and_referer_check_failed,api_missing_origin_and_referer,api_origin_mismatch,api_request_referer_check_failed,unknown_error", + "type": "custom" + }, + { + "allValue": ".*", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "SELECT name FROM \"auth_manager\".\"client\"", + "includeAll": true, + "label": "Subject", + "multi": true, + "name": "subject", + "options": [], + "query": "SELECT name FROM \"auth_manager\".\"client\"", + "refresh": 1, + "regex": "", + "sort": 1, + "staticOptions": [ + { + "text": "N/A", + "value": "N/A" + } + ], + "type": "query" + }, + { + "allValue": ".*", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select name from \"auth_manager\".\"domain\"", + "includeAll": true, + "label": "Domains", + "multi": true, + "name": "domains", + "options": [], + "query": "select name from \"auth_manager\".\"domain\"", + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Decision logs", + "uid": "opala-decision-logs", + "version": 17 +} diff --git a/helm/dashboards/opa.json b/helm/dashboards/opa.json new file mode 100644 index 00000000..9c4be7e8 --- /dev/null +++ b/helm/dashboards/opa.json @@ -0,0 +1,3069 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Open Policy Agents Runtime metrics", + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 11, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 49, + "maxDataPoints": 100, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "editorMode": "code", + "expr": "sum(rate(http_request_duration_seconds_sum{instance=~\"$instance\", namespace=\"$namespace\"}[$interval])) / sum(rate(http_request_duration_seconds_count{instance=~\"$instance\", namespace=\"$namespace\"}[$interval]))", + "format": "time_series", + "interval": "$interval", + "intervalFactor": 1, + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "Avg Response Time", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 47, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": " histogram_quantile(0.5, sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\", namespace=\"$namespace\"}[$interval])) by (le))", + "format": "time_series", + "interval": "$interval", + "intervalFactor": 1, + "legendFormat": "50th Quantile (Median)", + "refId": "A" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "histogram_quantile(0.9, sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\", namespace=\"$namespace\"}[$interval])) by (le))", + "format": "time_series", + "interval": "$interval", + "intervalFactor": 1, + "legendFormat": "90th Quantile", + "refId": "B" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\", namespace=\"$namespace\"}[$interval])) by (le))", + "format": "time_series", + "interval": "$interval", + "intervalFactor": 1, + "legendFormat": "95th Quantile", + "refId": "C" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\", namespace=\"$namespace\"}[$interval])) by (le))", + "format": "time_series", + "interval": "$interval", + "intervalFactor": 1, + "legendFormat": "99th Quantile", + "refId": "D" + } + ], + "title": "HTTP Response Time", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 15, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 36, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mspan_inuse_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mspan_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "B" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mcache_inuse_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "C" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mcache_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "D" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_buck_hash_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "E" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_gc_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "F" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_other_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"} - go_memstats_other_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "bytes of memory are used for other runtime allocations {pod={{ "{{" }}instance}{{ "}}" }}", + "refId": "G" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_next_gc_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "H" + } + ], + "title": "Memory in Off-Heap", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 16, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Total Used Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 45, + "panels": [], + "title": "Object and Pointers", + "type": "row" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 9, + "x": 0, + "y": 16 + }, + "id": 24, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_stack_inuse_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + }, + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_stack_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "B" + } + ], + "title": "Memory in Stack", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "description": "go_memstats_lookups_total – counts how many pointer dereferences happened. This is a counter value so you can use rate() to lookups/s.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 9, + "y": 16 + }, + "id": 18, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "rate(go_memstats_lookups_total{namespace=\"$namespace\", instance=~\"$instance\"}[$interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Rate of a Pointer Dereferences per $interval", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 9, + "x": 15, + "y": 16 + }, + "id": 22, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mallocs_total{namespace=\"$namespace\", instance=~\"$instance\"} - go_memstats_frees_total{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Number of Live Objects", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "description": "shows how many heap objects are allocated. This is a counter value so you can use rate() to objects allocated/s.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 23 + }, + "id": 20, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "rate(go_memstats_mallocs_total{namespace=\"$namespace\", instance=~\"$instance\"}[$interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Rate of Objects Allocated Per $interval", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 23 + }, + "id": 14, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "rate(go_memstats_alloc_bytes_total{namespace=\"$namespace\", instance=~\"$instance\"}[$interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Rates of Allocation per $interval", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 30, + "panels": [], + "title": "Memory In Heap", + "type": "row" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 12, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_heap_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Bytes obtained from system", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 32, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_heap_alloc_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "B" + } + ], + "title": "Bytes allocated & still in-use", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 33, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_heap_idle_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "C" + } + ], + "title": "Bytes waiting to be used", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 31, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_heap_inuse_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "D" + } + ], + "title": "Bytes in use ", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 34, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_heap_released_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "E" + } + ], + "title": "Bytes released to OS", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 35, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_heap_objects{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "B" + } + ], + "title": "Objects allocated", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 56 + }, + "id": 28, + "panels": [], + "title": "Memory Off-heap", + "type": "row" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 26, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mspan_inuse_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Mspan bytes usage", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 43, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mspan_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "B" + } + ], + "title": "Bytes used for mspan obtained from system", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 41, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mcache_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "D" + } + ], + "title": "Bytes used for mcache obtained from system", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 42, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_mcache_inuse_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "C" + } + ], + "title": "Bytes in use by mcache", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 39, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_gc_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "F" + } + ], + "title": "Bytes used for garbage collection system metadata", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 40, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_buck_hash_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "E" + } + ], + "title": "Bytes used by the profiling bucket hash table", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 37, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_next_gc_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "H" + } + ], + "title": "Bytes when next garbage collection will take place", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsNull", + "value": 0 + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": true, + "tooltip": true, + "viz": false + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 38, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_memstats_other_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"} - go_memstats_other_sys_bytes{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "bytes of memory are used for other runtime allocations {pod={{ "{{" }}instance}{{ "}}" }}", + "refId": "G" + } + ], + "title": "Bytes used for other system allocations", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 89 + }, + "id": 8, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_goroutines{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "Goroutines", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$cluster" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 89 + }, + "id": 4, + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "uid": "$cluster" + }, + "expr": "go_gc_duration_seconds{namespace=\"$namespace\", instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ "{{" }}instance{{ "}}" }}", + "refId": "A" + } + ], + "title": "GC duration quantile", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 42, + "tags": [ + "go", + "golang", + "opa", + "OPAla" + ], + "templating": { + "list": [ + { + "current": { + "text": "infra-dev", + "value": "infra-dev" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(go_goroutines{app=\"opa\"},namespace)", + "includeAll": false, + "label": "namespace", + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(go_goroutines{app=\"opa\"},namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(go_goroutines{namespace=\"$namespace\", app=~\"opa.*\"}, instance)", + "includeAll": true, + "label": "instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(go_goroutines{namespace=\"$namespace\", app=~\"opa.*\"}, instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "Prometheus", + "value": "prometheus" + }, + "includeAll": false, + "label": "Cluster", + "name": "cluster", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "2m", + "current": { + "text": "$__auto", + "value": "$__auto" + }, + "label": "interval", + "name": "interval", + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "2m", + "value": "2m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,2m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Open Policy Agent Metrics", + "uid": "opala-opa-metrics", + "version": 1 +} diff --git a/helm/dashboards/overview.json b/helm/dashboards/overview.json new file mode 100644 index 00000000..fbdf4f5b --- /dev/null +++ b/helm/dashboards/overview.json @@ -0,0 +1,1340 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 313, + "links": [ + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Performance", + "tooltip": "OPA throughput, latency, error rate, decision outcomes", + "type": "link", + "url": "/d/opala-performance/performance" + }, + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Bundle Status", + "tooltip": "Per-pod bundle revision vs. DB, staleness/liveness", + "type": "link", + "url": "/d/opala-bundle-status/bundle-status" + }, + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Denial Analytics", + "tooltip": "Aggregated denial/warning statistics by subject, domain, error code", + "type": "link", + "url": "/d/opala-denial-analytics/denial-analytics" + }, + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Decision Logs", + "tooltip": "Denial-investigation tool (denied/warning decisions only)", + "type": "link", + "url": "/d/opala-decision-logs/decision-logs" + } + ], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "### System Overview\n\nOne-glance health for **OPA**, **auth-manager**, and **auth-cron**. Each section links to a dedicated dashboard for detail.\n\nUse the **Environment** selector (`development` / `staging` / `production`) to pick a target environment. OPA, decision, and bundle panels are scoped to that environment; auth-manager and auth-cron run one instance per cluster serving every environment, so their health table is not scoped.\n\nBundle freshness flags any OPA pod running a bundle other than the one Postgres built for its environment. Decision outcomes come from the log pipeline and appear only where it runs.", + "mode": "markdown" + }, + "pluginVersion": "12.3.1", + "title": "", + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 4 + }, + "id": 10, + "panels": [], + "title": "Whole-system health", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "OPA pods running in the namespace that serves the selected environment.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 5, + "x": 0, + "y": 5 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "count(last_success_bundle_activation{namespace=~\"$ns\"}) or vector(0)", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "OPA pods serving $env", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Container restarts in the last hour across OPA pods serving the selected environment.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 5, + "x": 5, + "y": 5 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum(increase(kube_pod_container_status_restarts_total{namespace=~\"$ns\"}[1h]) and on(pod) label_replace(last_success_bundle_activation, \"pod\", \"$1\", \"instance\", \"(.*)\")) or vector(0)", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "OPA restarts ($env, 1h)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "auth-manager and auth-cron run one instance per cluster, not per environment, so this table is not affected by the environment selector. Health is red when ready replicas are below desired.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Health" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "color": "green", + "text": "✅ OK" + } + }, + "type": "value" + }, + { + "options": { + "from": -999, + "result": { + "color": "red", + "text": "⚠ Short" + }, + "to": -1 + }, + "type": "range" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Restarts" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 1 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Deployment" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open deployment in OpenShift", + "url": "${console_url}/k8s/ns/${__data.fields.Namespace}/deployments/${__data.fields.Deployment}" + } + ] + }, + { + "id": "custom.width", + "value": 196 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Ready" + }, + "properties": [ + { + "id": "custom.width", + "value": 61 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Desired" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 14, + "x": 10, + "y": 5 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (deployment, namespace) (kube_deployment_status_replicas_ready{deployment=~\".*-auth-manager$|.*-auth-cron$\"})", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (deployment, namespace) (kube_deployment_spec_replicas{deployment=~\".*-auth-manager$|.*-auth-cron$\"})", + "format": "table", + "instant": true, + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "(max by (deployment, namespace) (kube_deployment_status_replicas_ready{deployment=~\".*-auth-manager$|.*-auth-cron$\"})) - on(deployment, namespace) (max by (deployment, namespace) (kube_deployment_spec_replicas{deployment=~\".*-auth-manager$|.*-auth-cron$\"}))", + "format": "table", + "instant": true, + "range": false, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (deployment, namespace) (label_replace(kube_pod_container_status_restarts_total{pod=~\".*-auth-(manager|cron)-.*\"}, \"deployment\", \"$1\", \"pod\", \"^(.*-auth-(?:manager|cron))-.*$\"))", + "format": "table", + "instant": true, + "range": false, + "refId": "D" + } + ], + "title": "auth-manager & auth-cron health", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "renameByName": { + "Value #A": "Ready", + "Value #B": "Desired", + "Value #C": "Health", + "Value #D": "Restarts", + "deployment": "Deployment", + "namespace": "Namespace" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 20, + "panels": [], + "title": "Decision health", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Allow / deny / warn decision rate for the selected environment, from the decision-log metric pipeline. Only populated where that pipeline runs. Open Denial Analytics for aggregated statistics by subject and error code, or Decision Logs to investigate individual denials.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "allowed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "denied" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "warning" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 12 + }, + "id": 21, + "links": [ + { + "targetBlank": true, + "title": "Open Denial Analytics", + "url": "/d/opala-denial-analytics/denial-analytics" + }, + { + "targetBlank": true, + "title": "Open Decision Logs", + "url": "/d/opala-decision-logs/decision-logs" + } + ], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (status) (rate(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}[5m]))", + "legendFormat": "{{ "{{" }}status{{ "}}" }}", + "range": true, + "refId": "A" + } + ], + "title": "Decision rate by status ($env)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Current denied-decision rate for the selected environment. Zero or empty when no denials are flowing through the log pipeline.", + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "—", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 12 + }, + "id": 22, + "links": [ + { + "targetBlank": true, + "title": "Open Denial Analytics", + "url": "/d/opala-denial-analytics/denial-analytics" + }, + { + "targetBlank": true, + "title": "Open Decision Logs", + "url": "/d/opala-decision-logs/decision-logs" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum(rate(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\",status=\"denied\"}[5m]))", + "legendFormat": "denied/s", + "range": true, + "refId": "A" + } + ], + "title": "Denied rate ($env)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 30, + "panels": [], + "title": "Performance", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Decision requests per second across OPA pods serving the selected environment.", + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "—", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 20 + }, + "id": 31, + "links": [ + { + "targetBlank": true, + "title": "Open Performance", + "url": "/d/opala-performance/performance" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum(rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "legendFormat": "req/s", + "range": true, + "refId": "A" + } + ], + "title": "OPA decision rate ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "99th-percentile latency of decision requests for OPA pods serving the selected environment.", + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "—", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 0.5 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 20 + }, + "id": 32, + "links": [ + { + "targetBlank": true, + "title": "Open Performance", + "url": "/d/opala-performance/performance" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{handler=\"v1/data\", namespace=~\"$ns\"}[5m])))", + "legendFormat": "p99", + "range": true, + "refId": "A" + } + ], + "title": "Decision latency p99 ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Share of decision requests returning a 5xx response, for OPA pods serving the selected environment.", + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "—", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 20 + }, + "id": 33, + "links": [ + { + "targetBlank": true, + "title": "Open Performance", + "url": "/d/opala-performance/performance" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "100 * sum(rate(http_request_duration_seconds_count{handler=\"v1/data\",code=~\"5..\", namespace=~\"$ns\"}[5m])) / sum(rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "legendFormat": "error %", + "range": true, + "refId": "A" + } + ], + "title": "Error rate 5xx ($env)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 40, + "panels": [], + "title": "Bundle freshness", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "OPA pods serving the selected environment whose active bundle revision is not the latest revision Postgres built for that environment. Above zero means at least one pod is running the wrong or an outdated bundle.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 27 + }, + "id": 41, + "links": [ + { + "targetBlank": true, + "title": "Open Bundle Status", + "url": "/d/opala-bundle-status/bundle-status" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "count((max by (instance) (last_success_bundle_activation{namespace=~\"$ns\"} unless on(active_revision) label_replace(vector(1), \"active_revision\", \"$latest_revision\", \"\", \"\"))) * 0 + 1 or (max by (instance) (((time() - last_bundle_request/1e9) > ($grace_window_minutes * 60)) and on(instance) last_success_bundle_activation{namespace=~\"$ns\"})) * 0 + 1) or vector(0)", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "Stale pods ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "description": "Latest bundle revision Postgres built for the selected environment — the desired state every OPA pod for this environment should converge to.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 6, + "y": 27 + }, + "id": 42, + "links": [ + { + "targetBlank": true, + "title": "Open Bundle Status", + "url": "/d/opala-bundle-status/bundle-status" + } + ], + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "format": "table", + "rawSql": "select revision from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refId": "A" + } + ], + "title": "Latest DB revision ($env)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Seconds since the most-recently-polling OPA pod serving the selected environment last asked for a bundle. Rising above the threshold means a pod has stopped polling and will silently stop receiving new bundles.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 180 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 15, + "y": 27 + }, + "id": 43, + "links": [ + { + "targetBlank": true, + "title": "Open Bundle Status", + "url": "/d/opala-bundle-status/bundle-status" + } + ], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "min(time() - (last_bundle_request{namespace=~\"$ns\"} / 1e9))", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "Pod poll liveness ($env)", + "type": "stat" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 42, + "tags": [ + "OPAla" + ], + "templating": { + "list": [ + { + "current": { + "text": "infra-opala-{{ include "opa-la.environment" . }}", + "value": "infra-opala-{{ include "opa-la.environment" . }}" + }, + "hide": 2, + "label": "Postgres", + "name": "pg", + "options": [], + "query": "grafana-postgresql-datasource", + "refresh": 1, + "regex": "/.*opala.*/", + "type": "datasource" + }, + { + "current": { + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 2, + "label": "Prometheus", + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "/^Prometheus$/", + "type": "datasource" + }, + { + "current": { + "text": "np", + "value": "np" + }, + "includeAll": false, + "label": "Environment", + "name": "env", + "options": [ + { + "selected": true, + "text": "development", + "value": "np" + }, + { + "selected": false, + "text": "staging", + "value": "stage" + }, + { + "selected": false, + "text": "production", + "value": "prod" + } + ], + "query": "development : np,staging : stage,production : prod", + "type": "custom" + }, + { + "current": { + "text": "{{ .Values.dashboards.values.prod.namespace }}", + "value": "{{ .Values.dashboards.values.prod.namespace }}" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select (case '$env' when 'np' then '{{ .Values.dashboards.values.np.namespace }}' when 'stage' then '{{ .Values.dashboards.values.stage.namespace }}' when 'prod' then '{{ .Values.dashboards.values.prod.namespace }}' end) as ns", + "hide": 2, + "includeAll": false, + "label": "Namespace (mapped from env)", + "name": "ns", + "options": [], + "query": "select (case '$env' when 'np' then '{{ .Values.dashboards.values.np.namespace }}' when 'stage' then '{{ .Values.dashboards.values.stage.namespace }}' when 'prod' then '{{ .Values.dashboards.values.prod.namespace }}' end) as ns", + "refresh": 1, + "skipUrlSync": true, + "type": "query" + }, + { + "current": { + "text": "np-e9ed4a8eb683", + "value": "np-e9ed4a8eb683" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select revision from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "hide": 2, + "label": "Latest DB revision", + "name": "latest_revision", + "options": [], + "query": "select revision from \"auth_manager\".\"bundle\" where environment = '$env' order by created_at desc limit 1", + "refresh": 1, + "skipUrlSync": true, + "type": "query" + }, + { + "current": { + "text": "5", + "value": "5" + }, + "hide": 2, + "label": "Grace window (minutes)", + "name": "grace_window_minutes", + "query": "5", + "skipUrlSync": true, + "type": "constant" + }, + { + "current": { + "text": "{{ .Values.dashboards.values.prod.openshiftUrl }}", + "value": "{{ .Values.dashboards.values.prod.openshiftUrl }}" + }, + "hide": 2, + "includeAll": false, + "label": "OpenShift console", + "name": "console_url", + "options": [ + { + "selected": true, + "text": "np", + "value": "{{ .Values.dashboards.values.np.openshiftUrl }}" + }, + { + "selected": true, + "text": "stage", + "value": "{{ .Values.dashboards.values.stage.openshiftUrl }}" + }, + { + "selected": true, + "text": "prod", + "value": "{{ .Values.dashboards.values.prod.openshiftUrl }}" + } + ], + "query": "np : {{ .Values.dashboards.values.np.openshiftUrl }},stage : {{ .Values.dashboards.values.stage.openshiftUrl }},prod : {{ .Values.dashboards.values.prod.openshiftUrl }}", + "skipUrlSync": true, + "type": "custom" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "System Overview", + "uid": "opala-overview", + "version": 10 +} diff --git a/helm/dashboards/performance.json b/helm/dashboards/performance.json new file mode 100644 index 00000000..6ec8b840 --- /dev/null +++ b/helm/dashboards/performance.json @@ -0,0 +1,1773 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 302, + "links": [ + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Open Policy Agent Metrics (Go runtime internals)", + "tooltip": "Deep debugging: stock Go-runtime dashboard", + "type": "link", + "url": "/d/opala-opa-metrics/open-policy-agent-metrics" + }, + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Bundle Status", + "tooltip": "Per-pod bundle revision vs. DB, staleness/liveness", + "type": "link", + "url": "/d/opala-bundle-status/bundle-status" + }, + { + "asDashboard": false, + "icon": "dashboard", + "tags": [], + "targetBlank": true, + "title": "Decision Logs", + "tooltip": "Denial-investigation tool (denied/warning decisions only)", + "type": "link", + "url": "/d/opala-decision-logs/decision-logs" + } + ], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "### Performance\n\nOPA throughput, latency, error rate, and pod count for the selected environment; decision outcomes (allow/deny/warn); auth-manager/auth-cron health.\n\nOPA panels are scoped to the pods serving the selected environment. Decision Quality metrics come from the log pipeline and are only populated where it runs.", + "mode": "markdown" + }, + "pluginVersion": "12.3.1", + "title": "", + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 4 + }, + "id": 10, + "panels": [], + "title": "OPA — Native Performance", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Requests per second across OPA pods serving the selected environment.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 5 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum(rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "legendFormat": "req/s", + "range": true, + "refId": "A" + } + ], + "title": "Decision request rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 5 + }, + "id": 12, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "100 * sum(rate(http_request_duration_seconds_count{handler=\"v1/data\",code=~\"5..\", namespace=~\"$ns\"}[5m])) / sum(rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "legendFormat": "error %", + "range": true, + "refId": "A" + } + ], + "title": "Error rate (5xx)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "orange", + "value": 0.5 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 5 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{handler=\"v1/data\", namespace=~\"$ns\"}[5m])))", + "legendFormat": "p99", + "range": true, + "refId": "A" + } + ], + "title": "Latency p99 (v1/data)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Number of OPA pods currently serving the selected environment. Pods on an OPA version that doesn't yet report which environment it serves won't be counted until upgraded.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 5 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "count(last_success_bundle_activation{namespace=~\"$ns\"})", + "legendFormat": "matching pods", + "range": true, + "refId": "A" + } + ], + "title": "OPA pods", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 11 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (release, code) (rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "legendFormat": "{{ "{{" }}release{{ "}}" }} ({{ "{{" }}code{{ "}}" }})", + "range": true, + "refId": "A" + } + ], + "title": "Request rate by release & response code", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket{handler=\"v1/data\", namespace=~\"$ns\"}[5m])))", + "legendFormat": "p50", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{handler=\"v1/data\", namespace=~\"$ns\"}[5m])))", + "legendFormat": "p95", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{handler=\"v1/data\", namespace=~\"$ns\"}[5m])))", + "legendFormat": "p99", + "range": true, + "refId": "C" + } + ], + "title": "Latency percentiles (v1/data)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Pods currently serving the selected environment. Pods on an OPA version that doesn't yet report which environment it serves won't appear here until upgraded.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Pod" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open pod in OpenShift", + "url": "${console_url}/k8s/ns/${__data.fields.Namespace}/pods/${__data.fields.Pod}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Release" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open deployment in OpenShift", + "url": "${console_url}/k8s/ns/${__data.fields.Namespace}/deployments/${__data.fields.Release}-opa" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (instance, namespace, release) (rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "100 * sum by (instance, namespace, release) (rate(http_request_duration_seconds_count{handler=\"v1/data\",code=~\"5..\", namespace=~\"$ns\"}[5m])) / sum by (instance, namespace, release) (rate(http_request_duration_seconds_count{handler=\"v1/data\", namespace=~\"$ns\"}[5m]))", + "format": "table", + "instant": true, + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (instance, namespace, release, active_revision) (last_success_bundle_activation{namespace=~\"$ns\"})", + "format": "table", + "instant": true, + "range": false, + "refId": "C" + } + ], + "title": "OPA pod detail", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value #C": true + }, + "renameByName": { + "Value #A": "Req/s", + "Value #B": "Error %", + "Value #C": "", + "active_revision": "Active Revision", + "instance": "Pod", + "namespace": "Namespace", + "release": "Release" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 20, + "panels": [], + "title": "Decision Quality", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Loki never receives allowed decisions — this counter is the only source for allow/deny/warn rate.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 0, + "y": 26 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (status) (rate(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}[5m]))", + "legendFormat": "{{ "{{" }}status{{ "}}" }}", + "range": true, + "refId": "A" + } + ], + "title": "Decision rate by status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 10, + "y": 26 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, sum by (le) (rate(log_metric_histogram_opa_server_handler_ns_bucket{opa_env=\"${env:text}\"}[5m])))", + "legendFormat": "p50", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum by (le) (rate(log_metric_histogram_opa_server_handler_ns_bucket{opa_env=\"${env:text}\"}[5m])))", + "legendFormat": "p99", + "range": true, + "refId": "B" + } + ], + "title": "Decision handler latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Trust signal for the row above: if fluentbit drops records, decision-quality numbers silently undercount.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 0.001 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 26 + }, + "id": 23, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum(rate(fluentbit_filter_drop_records_total{namespace=~\"$ns\"}[5m]))", + "legendFormat": "dropped/s", + "range": true, + "refId": "A" + } + ], + "title": "Fluentbit drop rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 1000 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 30 + }, + "id": 24, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "fluentbit_input_http_server_ingress_queue_pending_events{namespace=~\"$ns\"}", + "legendFormat": "pending events", + "range": true, + "refId": "A" + } + ], + "title": "Fluentbit queue backlog", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Per-domain slice of decision volume for the selected environment.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 34 + }, + "id": 25, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (domain) (rate(log_metric_counter_opa_decisions_total{opa_env=\"${env:text}\"}[5m]))", + "legendFormat": "{{ "{{" }}domain{{ "}}" }}", + "range": true, + "refId": "A" + } + ], + "title": "Decisions by domain", + "type": "piechart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 30, + "panels": [], + "title": "auth-manager & auth-cron", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "auth-manager and auth-cron run one instance per Kubernetes namespace, not per environment, so this table isn't affected by the environment selector above.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Health" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "color": "green", + "text": "✅ OK" + } + }, + "type": "value" + }, + { + "options": { + "from": -999, + "result": { + "color": "red", + "text": "⚠ Short" + }, + "to": -1 + }, + "type": "range" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Deployment" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Open deployment in OpenShift", + "url": "${console_url}/k8s/ns/${__data.fields.Namespace}/deployments/${__data.fields.Deployment}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 31, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (deployment, namespace) (kube_deployment_status_replicas_ready{deployment=~\".*-auth-manager$|.*-auth-cron$\"})", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "max by (deployment, namespace) (kube_deployment_spec_replicas{deployment=~\".*-auth-manager$|.*-auth-cron$\"})", + "format": "table", + "instant": true, + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "(max by (deployment, namespace) (kube_deployment_status_replicas_ready{deployment=~\".*-auth-manager$|.*-auth-cron$\"})) - on(deployment, namespace) (max by (deployment, namespace) (kube_deployment_spec_replicas{deployment=~\".*-auth-manager$|.*-auth-cron$\"}))", + "format": "table", + "instant": true, + "range": false, + "refId": "C" + } + ], + "title": "", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "renameByName": { + "Value #A": "Ready", + "Value #B": "Desired", + "Value #C": "Health", + "deployment": "Deployment", + "namespace": "Namespace" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "sum by (operation) (rate(http_request_duration_seconds_count{app=\"auth-manager\"}[5m]))", + "legendFormat": "{{ "{{" }}operation{{ "}}" }}", + "range": true, + "refId": "A" + } + ], + "title": "auth-manager request rate by operation", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum by (le, operation) (rate(http_request_duration_seconds_bucket{app=\"auth-manager\"}[5m])))", + "legendFormat": "{{ "{{" }}operation{{ "}}" }}", + "range": true, + "refId": "A" + } + ], + "title": "auth-manager latency p95 by operation", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "nodejs_eventloop_lag_p99_seconds{app=~\"auth-manager|auth-cron\"}", + "legendFormat": "{{ "{{" }}app{{ "}}" }} ({{ "{{" }}namespace{{ "}}" }})", + "range": true, + "refId": "A" + } + ], + "title": "Event loop lag p99 (auth-manager, auth-cron)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Time since auth-cron/auth-manager last fetched their config. Informational only — not a measure of bundle freshness (see Bundle Status for that).", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "dtdurations" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 35, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "editorMode": "code", + "expr": "time() - config_time_of_last_fetch_unix_timestamp{app=~\"auth-manager|auth-cron\"}", + "legendFormat": "{{ "{{" }}app{{ "}}" }} ({{ "{{" }}namespace{{ "}}" }})", + "range": true, + "refId": "A" + } + ], + "title": "auth-cron config fetch age", + "type": "stat" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 42, + "tags": [ + "OPAla" + ], + "templating": { + "list": [ + { + "current": { + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 2, + "label": "Prometheus", + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "/^Prometheus$/", + "type": "datasource" + }, + { + "current": { + "text": "np", + "value": "np" + }, + "includeAll": false, + "label": "Environment", + "name": "env", + "options": [ + { + "selected": true, + "text": "development", + "value": "np" + }, + { + "selected": false, + "text": "staging", + "value": "stage" + }, + { + "selected": false, + "text": "production", + "value": "prod" + } + ], + "query": "development : np,staging : stage,production : prod", + "type": "custom" + }, + { + "current": { + "text": "{{ .Values.dashboards.values.prod.openshiftUrl }}", + "value": "{{ .Values.dashboards.values.prod.openshiftUrl }}" + }, + "hide": 2, + "includeAll": false, + "label": "OpenShift console", + "name": "console_url", + "options": [ + { + "selected": true, + "text": "np", + "value": "{{ .Values.dashboards.values.np.openshiftUrl }}" + }, + { + "selected": true, + "text": "stage", + "value": "{{ .Values.dashboards.values.stage.openshiftUrl }}" + }, + { + "selected": true, + "text": "prod", + "value": "{{ .Values.dashboards.values.prod.openshiftUrl }}" + } + ], + "query": "np : {{ .Values.dashboards.values.np.openshiftUrl }},stage : {{ .Values.dashboards.values.stage.openshiftUrl }},prod : {{ .Values.dashboards.values.prod.openshiftUrl }}", + "skipUrlSync": true, + "type": "custom" + }, + { + "current": { + "text": "infra-opala-{{ include "opa-la.environment" . }}", + "value": "infra-opala-{{ include "opa-la.environment" . }}" + }, + "hide": 2, + "label": "Postgres", + "name": "pg", + "options": [], + "query": "grafana-postgresql-datasource", + "refresh": 1, + "regex": "/.*opala.*/", + "type": "datasource" + }, + { + "current": { + "text": "{{ .Values.dashboards.values.prod.namespace }}", + "value": "{{ .Values.dashboards.values.prod.namespace }}" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "${pg}" + }, + "definition": "select (case '$env' when 'np' then '{{ .Values.dashboards.values.np.namespace }}' when 'stage' then '{{ .Values.dashboards.values.stage.namespace }}' when 'prod' then '{{ .Values.dashboards.values.prod.namespace }}' end) as ns", + "hide": 2, + "includeAll": false, + "label": "Namespace (mapped from env)", + "name": "ns", + "options": [], + "query": "select (case '$env' when 'np' then '{{ .Values.dashboards.values.np.namespace }}' when 'stage' then '{{ .Values.dashboards.values.stage.namespace }}' when 'prod' then '{{ .Values.dashboards.values.prod.namespace }}' end) as ns", + "refresh": 1, + "skipUrlSync": true, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Performance", + "uid": "opala-performance", + "version": 11 +} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 485e4e9c..3fdf5664 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -19,7 +19,7 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} -{{/* +{{/* Create chart name and version as used by the chart label. */}} {{- define "opa-la.chart" -}} @@ -57,11 +57,8 @@ app.kubernetes.io/instance: {{ .Release.Name }} Returns the environment from global if exists or from the chart's values, defaults to development */}} {{- define "opa-la.environment" -}} -{{- if .Values.global.environment }} - {{- .Values.global.environment -}} -{{- else -}} - {{- .Values.environment | default "development" -}} -{{- end -}} +{{- $global := dig "mclabels" "environment" "" ((.Values.global | default dict) | toJson | fromJson) -}} +{{- required "opa-la: environment is required. Set global.mclabels.environment (or .Values.environment)." (default .Values.environment $global) -}} {{- end -}} {{/* diff --git a/helm/templates/dashboards-secret.yaml b/helm/templates/dashboards-secret.yaml new file mode 100644 index 00000000..1d66b5d7 --- /dev/null +++ b/helm/templates/dashboards-secret.yaml @@ -0,0 +1,20 @@ +{{- $releaseName := .Release.Name -}} +{{- $chartName := include "opa-la.name" . -}} +{{- if .Values.dashboards.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $releaseName }}-dashboards-secret + labels: + mapcolonies.io/grafana-dashboards: "true" + annotations: + mapcolonies.io/grafana-dashboards-folder: "infra/opa-la" +type: Opaque +stringData: + overview.json: {{ tpl (.Files.Get "dashboards/overview.json") . | fromJson | toJson | quote }} + bundle.json: {{ tpl (.Files.Get "dashboards/bundle.json") . | fromJson | toJson | quote }} + performance.json: {{ tpl (.Files.Get "dashboards/performance.json") . | fromJson | toJson | quote }} + decision.json: {{ tpl (.Files.Get "dashboards/decision.json") . | fromJson | toJson | quote }} + analytics.json: {{ tpl (.Files.Get "dashboards/analytics.json") . | fromJson | toJson | quote }} + opa.json: {{ tpl (.Files.Get "dashboards/opa.json") . | fromJson | toJson | quote }} +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 1c563fa7..980cd10f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -24,6 +24,30 @@ tls: &tls key: '' caCertificate: '' +dashboards: + enabled: true + values: + np: + openshiftUrl: https://replace-me.com + namespace: dev + stage: + openshiftUrl: https://replace-me.com + namespace: stage + prod: + openshiftUrl: https://replace-me.com + namespace: prod + +grafana-datasources: + enabled: true + team: infra + instance: opala + datasources: + postgres: + - service: opala + host: localhost:5432 + database: auth-manager + username: postgres + password: postgres opa: enabled: true cloudProvider: *cloudProvider diff --git a/knip.config.ts b/knip.config.ts index bf85fb2d..ededdf15 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -10,7 +10,6 @@ const config: KnipConfig = { type: true, }, ignoreBinaries: ['helm'], - ignoreFiles: ['example/**'], ignoreDependencies: ['@map-colonies/infra-copilot-instructions', '@vitest/eslint-plugin'], workspaces: { 'packages/auth-core': { diff --git a/policy/logs.rego b/policy/logs.rego index 51b9eaea..d4a0271d 100644 --- a/policy/logs.rego +++ b/policy/logs.rego @@ -1,11 +1,24 @@ package system.log +allowed_request if { + input.result.allowed == true + input.result.warnings == [] +} + # Mask the 'token' field in the query parameters -mask contains "/input/query/token" +mask contains {"op": "upsert", "path": "/input/query/token", "value": "**redacted**"} if { + input.input.query.token + not allowed_request +} # Mask the 'x-api-key' field in the headers parameters -mask contains "/input/headers/x-api-key" +mask contains {"op": "upsert", "path": "/input/headers/x-api-key", "value": "**redacted**"} if { + input.input.headers["x-api-key"] + not allowed_request +} -drop if { - input.result.allowed == true +mask contains {"op": "upsert", "path": "/input", "value": r} if { + allowed_request + + r := {"domain": input.input.domain} } diff --git a/policy/policy.rego b/policy/policy.rego index 29723773..123027c6 100644 --- a/policy/policy.rego +++ b/policy/policy.rego @@ -1,102 +1,292 @@ +# METADATA +# title: HTTP Authorization Policy +# description: | +# Evaluates incoming HTTP requests against JWT token validity, user configuration, +# domain allowlists, origin allowlists, and user-agent requirements. +# Produces a `decision` document indicating whether the request is allowed or denied. +# scope: package +# labels: +# category: authz +# layer: gateway package http.authz +import future.keywords.not + +# METADATA +# title: Normalize Object Keys to Lowercase +# description: Returns a copy of an object with all keys lowercased, enabling case-insensitive header and input field lookups. lower_object_keys(obj) := {k: v | some i v := obj[i] k := lower(i) } +# METADATA +# title: JWT Verification Constraints +# description: RS256 algorithm and the active public key used to verify incoming JWTs. constraints := { "cert": json.marshal({"keys": [data.keys]}), "alg": "RS256", } -headers := lower_object_keys(input.headers) +# METADATA +# title: Chromium Brand Tokens +# description: Known Sec-CH-UA brand values sent by genuine Chromium-based browsers. +chromium_ch_ua_brands := ["Chromium", "Google Chrome", "Microsoft Edge", "Brave"] + +# METADATA +# title: Known Bot Patterns +# description: Substring patterns used to identify known automation tools in the User-Agent header. +bot_patterns := ["curl", "python", "wget", "java/", "go-http-client", "okhttp", "axios", "libwww"] +# METADATA +# title: Normalized Request Headers +# description: The incoming request headers with all keys lowercased for case-insensitive access. +headers := lower_object_keys(object.get(input, "headers", {})) + +# METADATA +# title: Extracted Tokens +# description: | +# Collects all tokens found at any of the configured possible locations (e.g. Authorization +# header, query param). Each location is a two-element path [container, key] defined in +# data.possibleLocations. tokens := [token | - # keys := data.possibleLocations[_] some key in data.possibleLocations lower_obj := lower_object_keys(object.get(input, key[0], "")) token := object.get(lower_obj, key[1], null) token != null ] +# METADATA +# title: Default Claims (No Token) +# description: Fallback claims value used when no valid token is present; payload defaults to empty. +default claims := {"payload": {}} + +# METADATA +# title: Decoded and Verified JWT Claims +# description: | +# Decodes the first matching token found in the request, extracting the payload, +# validity flag, and key ID (kid) from the JWT header. claims := {"payload": payload, "valid": valid, "kid": kid} if { - # token := tokens[_] some token in tokens [header, payload, _] := io.jwt.decode(token) kid := object.get(header, "kid", null) # Extract kid from JWT header [valid, _, _] := io.jwt.decode_verify(token, constraints) } +# METADATA +# title: C2B Service Account User Data +# description: | +# Hardcoded profile for the c2b machine-to-machine service account. +# Grants access only to the raster domain and bypasses origin and browser checks. user_data := {"domains": ["raster"], "allowNoOrigin": true, "allowNoBrowser": true, "origins": []} if { claims.payload.sub == "c2b" } +# METADATA +# title: User Data Lookup +# description: Retrieves the user configuration record from the data store by the token subject claim. user_data := data.users[claims.payload.sub] if { claims.payload.sub != "c2b" } +# METADATA +# title: Origin Validation Helper +# description: | +# Returns true when the request origin does not match an allowed origin entry, +# supporting both exact string match and glob patterns. is_origin_invalid(origin_header, allowed_origin) if { allowed_origin != origin_header - not glob.match(allowed_origin, [], origin_header) + + # Use "." as delimiter so "*" only matches a single domain label. + # e.g. "*.example.com" matches "sub.example.com" but NOT "a.b.example.com". + not glob.match(allowed_origin, ["."], origin_header) +} + +extract_host(url) := host if { + without_scheme := regex.replace(url, `^https?://`, "") + host := split(without_scheme, "/")[0] + + # Guard: only process URLs that actually started with http(s):// + regex.match(`^https?://`, url) +} + +extract_host(url) := "__invalid__" if { + url != "" + not startswith(url, "http://") + not startswith(url, "https://") +} + +# METADATA +# title: IE11 Detection Helper +# description: True when the User-Agent contains the IE11 Trident/7.0 engine token. +is_ie11 if { + regex.match(`Trident/7\.0`, headers["user-agent"]) +} + +## METADATA +# title: Image Fetch Detection Helper +# description: True when Sec-Fetch-Dest indicates an image or paintworklet subresource request. + +# is_image_fetch if { +# lower(object.get(headers, "sec-fetch-dest", "")) in {"image", "paintworklet"} +# } + +## METADATA +# title: API Fetch Detection Helper +# description: True when Sec-Fetch-Dest is empty or absent, indicating an XHR/fetch API request. + +# is_api_fetch if { +# dest := lower(object.get(headers, "sec-fetch-dest", "")) +# dest in {"empty", ""} +# not is_image_fetch +# not is_ie11 +# } + +# METADATA +# title: Referer Origin Invalid Helper +# description: | +# True when the Referer header is present but its bare host does not match any of the +# user's allowed origins. Shared by the IE11, image, and API fallback deny rules. +is_referer_origin_invalid if { + user_data + referer_header := object.get(headers, "referer", "") + referer_header != "" + ref_host := extract_host(referer_header) + every o in user_data.origins { is_origin_invalid(ref_host, o) } } +# METADATA +# title: User-Agent Required (Non-Browser-Exempt Users) +# description: True when the user's profile does not grant browser exemption, requiring a User-Agent header. need_user_agent if { not user_data.allowNoBrowser } +# METADATA +# title: User-Agent Required (C2B) +# description: The c2b service account always requires a User-Agent to enforce the QGIS/ARCGIS check. need_user_agent if { claims.payload.sub == "c2b" } -deny contains "no token supplied in any of the possible locations" if { - count(tokens) == 0 +# METADATA +# title: Warn — Token Environment Mismatch +# description: | +# Warns when the kid in the token does not match the expected key ID for this environment, +# preventing cross-environment token reuse. +warn contains "token environment mismatch" if { + claims.kid != data.keys.kid } -deny contains "token environment mismatch" if { - claims.kid != data.keys.kid +# METADATA +# title: Warn — IE11 Legacy Browser Detected +# description: | +# Logs a warning when an IE11 request passes the origin/referer check, signalling +# that a legacy browser is in use. The request is allowed to proceed. +warn contains "ie11 legacy browser detected" if { + user_data + is_ie11 + claims.payload.sub != "c2b" + + # OPA evaluates deny and warn as independent sets — no circular dependency. + not deny[{"reason": "ie11 missing origin and referer", "code": "ie11_missing_origin_and_referer"}] + not deny[{"reason": "ie11 origin and referer check failed", "code": "ie11_origin_and_referer_check_failed"}] } -deny contains "token not valid" if { +# METADATA +# title: Deny — No Token Found +# description: Denies the request when no token is present in any of the configured possible locations. +# labels: +# group: token-missing +# category: authentication +# type: missing +deny contains {"reason": "no token supplied in any of the possible locations", "code": "no_token_supplied"} if { + count(tokens) == 0 +} + +# METADATA +# title: Deny — Token Not Valid +# description: Denies when a token was found but fails cryptographic verification (expired, bad signature, wrong issuer, etc.). +# labels: +# group: token-invalid +# category: authentication +# type: invalid +deny contains {"reason": "token not valid", "code": "token_not_valid"} if { not claims.valid count(tokens) > 0 } -deny contains "the token is valid, but the user is not found" if { +# METADATA +# title: Deny — User Not Found +# description: | +# Denies when the token is cryptographically valid but the subject (sub) has no +# corresponding user record in the data store. +# labels: +# group: user-missing +# category: authorization +# type: missing +deny contains {"reason": "the token is valid, but the user is not found", "code": "user_not_found"} if { claims.valid not user_data } -deny contains "domain missing" if { +# METADATA +# title: Deny — Domain Missing +# description: Denies when the request does not include a domain field, which is required for domain allowlist evaluation. +# labels: +# group: domain-missing +# category: authorization +# type: missing +deny contains {"reason": "domain missing", "code": "domain_missing"} if { not input.domain } -deny contains "domain check failed" if { +# METADATA +# title: Deny — Domain Not Allowed +# description: Denies when the requested domain is not present in the user's allowed domains list. +# labels: +# group: domain-unauthorized +# category: authorization +# type: unauthorized +deny contains {"reason": $"domain check failed: '{input.domain}' is not in the allowed domains {concat(", ", user_data.domains)}", "code": "domain_check_failed"} if { every domain in user_data.domains { domain != input.domain } } -deny contains "origin check failed" if { - origin_header := object.get(headers, "origin", "A") - every origin in user_data.origins { is_origin_invalid(origin_header, origin) } - - not user_data.allowNoOrigin - claims.payload.sub != "c2b" - - object.get(headers, "sec-fetch-site", "avi") != "same-origin" -} - -deny contains "user-agent missing" if { +# METADATA +# title: Deny — User-Agent Missing +# description: Denies when no User-Agent header is present and a User-Agent is required for the caller. +# labels: +# group: user-agent-missing +# category: browser-validation +# type: missing +deny contains {"reason": "user-agent missing", "code": "user_agent_missing"} if { not headers["user-agent"] need_user_agent } -deny contains "user-agent is not from allowed browsers" if { +# METADATA +# title: Deny — User-Agent Not From Allowed Browser +# description: | +# Denies when the User-Agent does not match any known browser engine token +# (Gecko, AppleWebKit, Opera, Trident, Edge, Chrome). +# labels: +# group: user-agent-unauthorized +# category: browser-validation +# type: unauthorized +deny contains {"reason": $`user-agent is not from allowed browsers: '{headers["user-agent"]}'`, "code": "user_agent_not_allowed"} if { not user_data.allowNoBrowser not regex.match(`.*(Gecko|AppleWebKit|Opera|Trident|Edge|Chrome)\/\d.*$`, headers["user-agent"]) } -deny contains "c2b user only allowed from QGIS or ARCGIS" if { +# METADATA +# title: Deny — C2B Client Not QGIS or ARCGIS +# description: Denies c2b service account requests that do not originate from a QGIS or ARCGIS user agent. +# labels: +# group: user-agent-unauthorized +# category: browser-validation +# type: unauthorized +deny contains {"reason": $"c2b user only allowed from QGIS or ARCGIS, got: '{user_agent}'", "code": "c2b_ua_not_allowed"} if { claims.payload.sub == "c2b" user_agent := lower(headers["user-agent"]) @@ -106,13 +296,277 @@ deny contains "c2b user only allowed from QGIS or ARCGIS" if { } # METADATA +# title: Deny — Bot User-Agent Detected +# description: | +# Denies requests whose User-Agent matches a known automation tool pattern. +# The matched pattern is included in the deny reason for operator visibility. +# labels: +# group: user-agent-bot +# category: browser-validation +# type: unauthorized +deny contains {"reason": $"bot user-agent detected: {pattern}", "code": "bot_user_agent_detected"} if { + user_data + not user_data.allowNoBrowser + some pattern in bot_patterns + contains(lower(headers["user-agent"]), pattern) +} + +## METADATA +# title: Deny — Sec-Fetch-Dest Image Request Does Not Accept Image Content +# description: | +# Denies requests that declare an image destination (Sec-Fetch-Dest: image/paintworklet) +# but do not accept image MIME types in the Accept header. This inconsistency indicates +# non-browser automation spoofing an image fetch. +# labels: +# group: mime-mismatch +# category: browser-validation +# type: unauthorized + +# deny contains "sec-fetch-dest image request does not accept image content" if { +# user_data +# not user_data.allowNoBrowser +# sec_fetch_dest := lower(object.get(headers, "sec-fetch-dest", "")) +# sec_fetch_dest in {"image", "paintworklet"} +# accept := object.get(headers, "accept", "") +# not contains(accept, "image/") +# } + +## METADATA +# title: Deny — Chromium Browser Missing Sec-Fetch Headers +# description: | +# Denies Chromium-based requests that lack Sec-Fetch-Site. Genuine browser requests +# from Chromium always include Sec-Fetch headers; absence implies UA spoofing. +# labels: +# group: user-agent-metadata +# category: browser-validation +# type: unauthorized + +# deny contains "chromium browser missing sec-fetch headers" if { +# user_data +# not user_data.allowNoBrowser +# regex.match(`Chrome/\d`, headers["user-agent"]) +# not headers["sec-fetch-site"] +# } + +## METADATA +# title: Deny — Sec-CH-UA Header Contradicts User-Agent +# description: | +# Denies when Sec-CH-UA is present and the UA claims Chromium but no known Chromium +# brand token appears in Sec-CH-UA, indicating a forged or mismatched header pair. +# labels: +# group: user-agent-metadata +# category: browser-validation +# type: unauthorized + +# deny contains "sec-ch-ua header contradicts user-agent" if { +# user_data +# not user_data.allowNoBrowser +# regex.match(`Chrome/\d`, headers["user-agent"]) +# sec_ch_ua := object.get(headers, "sec-ch-ua", "") +# sec_ch_ua != "" +# every brand in chromium_ch_ua_brands { +# not contains(sec_ch_ua, brand) +# } +# } + +## METADATA +# title: Warn — Safari or Firefox UA with Sec-CH-UA (Chromium-Only Header) +# description: | +# Logs a soft warning when a Safari or Firefox UA includes Sec-CH-UA, which is a +# Chromium-only header. This is a potential spoofing signal; the request is allowed. + +# warn contains "safari or firefox ua with sec-ch-ua header (chromium-only header)" if { +# user_data +# sec_ch_ua := object.get(headers, "sec-ch-ua", "") +# sec_ch_ua != "" +# ua := lower(object.get(headers, "user-agent", "")) +# regex.match(`(safari|firefox)\/\d`, ua) +# not regex.match(`chrome\/\d`, ua) # exclude Chrome-on-Safari-engine (e.g. iOS Chrome) +# } + +## METADATA +# title: Deny — No Origins Configured for User +# description: | +# Denies when the user requires origin validation (allowNoOrigin is false) but has +# no entries in their origins list. This is a misconfiguration: without any allowed +# origins the user can never be granted access, so deny early with a clear reason. +# labels: +# group: origin-misconfigured +# category: authorization +# type: misconfigured + +deny contains {"reason": "user has no allowed origins configured", "code": "no_origins_configured"} if { + user_data + not user_data.allowNoOrigin + claims.payload.sub != "c2b" + count(user_data.origins) == 0 +} + +# METADATA +# title: Deny — IE11 Missing Origin and Referer +# description: Denies IE11 requests where neither Origin nor Referer header is present (no identity). +# labels: +# group: origin-missing +# category: authorization +# type: missing +deny contains {"reason": "ie11 missing origin and referer", "code": "ie11_missing_origin_and_referer"} if { + user_data + is_ie11 + not user_data.allowNoOrigin + claims.payload.sub != "c2b" + not headers.origin + not headers.referer +} + +## METADATA +# title: Deny — IE11 Origin and Referer Check Failed +# description: | +# Denies IE11 requests where at least one identity header was present but none matched +# an allowed domain. +# labels: +# group: origin-unauthorized +# category: authorization +# type: unauthorized + +# deny contains "ie11 origin and referer check failed" if { +# is_ie11 +# not user_data.allowNoOrigin +# claims.payload.sub != "c2b" + +# # At least one header was present (the fully-absent case is handled above). +# some _ in [object.get(headers, "origin", null), object.get(headers, "referer", null)] + +# # Strip scheme from origin before comparing against domain-only entries in user data. +# # extract_host("__missing__") returns "__invalid__" so absent origin is always invalid. +# origin_host := extract_host(object.get(headers, "origin", "__missing__")) +# every o in user_data.origins { is_origin_invalid(origin_host, o) } + +# is_referer_origin_invalid +# } + +## METADATA +# title: Warn — Image Request Missing Referer +# description: Image subresource request arrived without a Referer header; identity is unknown. + +# warn contains "image request missing referer" if { +# user_data +# is_image_fetch +# not user_data.allowNoOrigin +# claims.payload.sub != "c2b" +# object.get(headers, "sec-fetch-site", "") != "same-origin" +# not headers["referer"] +# # Only warn when the deny rule is not also firing. +# not deny["image request referer check failed"] +# } + +## METADATA +# title: Deny — Image Request Referer Check Failed +# description: Denies image subresource requests whose Referer origin does not match an allowed domain. +# labels: +# group: origin-unauthorized +# category: authorization +# type: unauthorized + +# deny contains "image request referer check failed" if { +# is_image_fetch +# not user_data.allowNoOrigin +# claims.payload.sub != "c2b" +# object.get(headers, "sec-fetch-site", "") != "same-origin" +# is_referer_origin_invalid +# } + +# METADATA +# title: Deny — API Request Missing Origin and Referer +# description: | +# Denies API/XHR requests where no identity header (Origin or Referer) was sent at all. +# labels: +# group: origin-missing +# category: authorization +# type: missing +deny contains {"reason": "api request missing origin and referer", "code": "api_missing_origin_and_referer"} if { + user_data + + # is_api_fetch + not user_data.allowNoOrigin + claims.payload.sub != "c2b" + + # object.get(headers, "sec-fetch-site", "") != "same-origin" + not headers.origin + not headers.referer +} + +# METADATA +# title: Deny — API Request Origin Mismatch +# description: | +# Denies API/XHR requests where Origin is present but does not match any allowed domain. +# labels: +# group: origin-unauthorized +# category: authorization +# type: unauthorized +deny contains {"reason": $"api request origin mismatch: '{origin_host}' is not in the allowed origins {user_data.origins}", "code": "api_origin_mismatch"} if { + # is_api_fetch + not user_data.allowNoOrigin + claims.payload.sub != "c2b" + + # object.get(headers, "sec-fetch-site", "") != "same-origin" + # Strip scheme: Origin header sends "https://host" but data stores host-only. + origin_raw := object.get(headers, "origin", "") + origin_raw != "" + origin_host := extract_host(origin_raw) + every o in user_data.origins { is_origin_invalid(origin_host, o) } +} + +# METADATA +# title: Deny — API Request Referer Fallback Check Failed +# description: | +# Denies API/XHR requests where Origin is absent and the Referer fallback origin does +# not match any allowed domain. +# labels: +# group: origin-unauthorized +# category: authorization +# type: unauthorized +deny contains {"reason": "api request referer fallback check failed", "code": "api_request_referer_check_failed"} if { + # is_api_fetch + not user_data.allowNoOrigin + claims.payload.sub != "c2b" + object.get(headers, "sec-fetch-site", "") != "same-origin" + not headers.origin + is_referer_origin_invalid +} + +metadata := { + "sub": object.get(claims.payload, "sub", "N/A"), + "kid": object.get(claims, "kid", "N/A"), +} + +default warnings := [] + +warnings := warn if { + count(warn) > 0 +} + +default computed_status := "allowed" + +computed_status := "warning" if { + count(warn) > 0 +} + +default decision := {"allowed": false, "reasons": ["unknown error"], "codes": ["unknown_error"], "computed_status": "denied"} + +# METADATA +# title: Authorization Decision +# description: | +# Policy entrypoint. Produces allowed:true with the subject and kid when all checks pass, +# or allowed:false with a concatenated reason string when any deny rule fires. # entrypoint: true -decision := {"allowed": true, "sub": claims.payload.sub, "kid": claims.kid} if { +decision := {"allowed": true, "metadata": metadata, "warnings": warnings, "computed_status": computed_status} if { count(deny) == 0 claims.payload.sub != null } -decision := {"allowed": false, "reason": reason} if { +decision := {"allowed": false, "metadata": metadata, "reasons": reasons, "codes": codes, "warnings": warnings, "computed_status": computed_status} if { count(deny) > 0 - reason := concat(", ", deny) + reasons := [d.reason | some d in deny] + codes := [d.code | some d in deny] + computed_status := "denied" } diff --git a/policy/policy_test.rego b/policy/policy_test.rego index 98404278..235c04d2 100644 --- a/policy/policy_test.rego +++ b/policy/policy_test.rego @@ -49,7 +49,7 @@ users := { "allowNoBrowser": false, "allowNoOrigin": false, "domains": ["avi"], - "origins": ["https://avi.com"], + "origins": ["avi.com"], }, "itzik": { "allowNoBrowser": false, @@ -61,10 +61,12 @@ users := { "allowNoBrowser": false, "allowNoOrigin": false, "domains": ["avi"], - "origins": ["https://google.com", "*.zvika.maps.com"], + "origins": ["google.com", "*.zvika.maps.com"], }, } +possible_locations := [["headers", "x-api-key"], ["query", "token"]] + generate_token(key, payload) := token if { new_payload := json.patch(payload, [{"op": "add", "path": "iss", "value": "mapcolonies-token-cli"}]) token := io.jwt.encode_sign( @@ -85,31 +87,34 @@ generate_token(key, payload) := token if { test_deny_no_token if { res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - res.reason == "no token supplied in any of the possible locations" + "no token supplied in any of the possible locations" in res.reasons } test_deny_malformed_token if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": substring(token, 0, 40), "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - res.reason == "token not valid" + "token not valid" in res.reasons } test_deny_token_signed_with_wrong_key if { token := generate_token(private_key_2, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "token environment mismatch") + "token environment mismatch" in res.warnings } # ========================================================= @@ -118,8 +123,9 @@ test_deny_token_signed_with_wrong_key if { test_allow_token_supplied_in_header if { token := generate_token(private_key_1, {"sub": "avi"}) - res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed @@ -127,8 +133,9 @@ test_allow_token_supplied_in_header if { test_allow_token_supplied_in_query_param if { token := generate_token(private_key_1, {"sub": "avi"}) - res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "User-Agent": chrome_agent}, "query": {"token": token}} + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}, "query": {"token": token}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed @@ -142,10 +149,11 @@ test_deny_user_not_found if { token := generate_token(private_key_1, {"sub": "nx_user"}) res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - res.reason == "the token is valid, but the user is not found" + "the token is valid, but the user is not found" in res.reasons } # ========================================================= @@ -156,20 +164,24 @@ test_deny_domain_missing_from_request if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "domain missing") + some _r in res.reasons + contains(_r, "domain missing") } test_deny_domain_not_allowed_for_user if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "itzik", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "domain check failed") + some _r in res.reasons + contains(_r, "domain check failed") } # ========================================================= @@ -180,6 +192,7 @@ test_deny_wrong_origin if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi2.com", "X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed @@ -189,25 +202,30 @@ test_deny_missing_origin if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed } # sec-fetch-site: same-origin bypasses origin header check (browser same-origin requests don't always send Origin) -test_allow_same_origin_fetch_site_bypasses_origin_check if { - token := generate_token(private_key_1, {"sub": "avi"}) - res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "same-origin"}} - with data.keys as public_key_1 - with data.users as users - - res.allowed -} +# test_allow_same_origin_fetch_site_bypasses_origin_check if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# # sec-fetch-site: same-origin satisfies Rule 2.2 (Sec-Fetch-Site is present) and +# # also bypasses the origin check — no Origin header needed. +# res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "same-origin"}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# res.allowed +# } test_allow_no_origin_when_user_permits_it if { token := generate_token(private_key_1, {"sub": "avi"}) - res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent}} + res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users with data.users.avi.allowNoOrigin as true @@ -216,8 +234,9 @@ test_allow_no_origin_when_user_permits_it if { test_allow_wildcard_origin_matching_subdomain if { token := generate_token(private_key_1, {"sub": "itzik"}) - res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Origin": "https://meow.itzik.maps.com"}} + res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Origin": "https://meow.itzik.maps.com", "Sec-Fetch-Site": "cross-site"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed @@ -227,6 +246,7 @@ test_deny_wildcard_origin_not_matching_subdomain if { token := generate_token(private_key_1, {"sub": "itzik"}) res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Origin": "https://meow.avi.maps.com"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed @@ -234,8 +254,9 @@ test_deny_wildcard_origin_not_matching_subdomain if { test_allow_specific_origin_for_user_with_multiple_origins if { token := generate_token(private_key_1, {"sub": "zvika"}) - res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Origin": "https://google.com"}} + res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Origin": "https://google.com", "Sec-Fetch-Site": "cross-site"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed @@ -245,6 +266,7 @@ test_deny_unrecognised_origin_for_user_with_multiple_origins if { token := generate_token(private_key_1, {"sub": "zvika"}) res := authz.decision with input as {"domain": "avi", "headers": {"User-Agent": chrome_agent, "Origin": "https://ggl.com"}, "query": {"token": token}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed @@ -254,6 +276,7 @@ test_deny_origin_matching_wrong_glob_pattern if { token := generate_token(private_key_1, {"sub": "zvika"}) res := authz.decision with input as {"domain": "avi", "headers": {"User-Agent": chrome_agent, "Origin": "https://avi.bla.zvika.maps.com"}, "query": {"token": token}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed @@ -267,26 +290,31 @@ test_deny_non_browser_user_agent if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": postman_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "user-agent is not from allowed browsers") + some _r in res.reasons + contains(_r, "user-agent is not from allowed browsers") } test_deny_missing_user_agent if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "user-agent missing") + some _r in res.reasons + contains(_r, "user-agent missing") } test_allow_non_browser_user_agent_when_user_permits_it if { token := generate_token(private_key_1, {"sub": "avi"}) res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": postman_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users with data.users.avi.allowNoBrowser as true with data.users.avi.allowNoOrigin as true @@ -300,22 +328,24 @@ test_allow_non_browser_user_agent_when_user_permits_it if { test_allowed_decision_contains_sub if { token := generate_token(private_key_1, {"sub": "avi"}) - res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed - res.sub == "avi" + res.metadata.sub == "avi" } test_allowed_decision_contains_kid if { token := generate_token(private_key_1, {"sub": "avi"}) - res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed - res.kid == "opa_test_key_1" + res.metadata.kid == "opa_test_key_1" } # ========================================================= @@ -326,6 +356,7 @@ test_allow_c2b_connection_from_qgis if { token := generate_token(private_key_1, {"sub": "c2b"}) res := authz.decision with input as {"domain": "raster", "headers": {"X-Api-Key": token, "User-Agent": qgis_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed @@ -335,6 +366,7 @@ test_allow_c2b_connection_from_arcgis if { token := generate_token(private_key_1, {"sub": "c2b"}) res := authz.decision with input as {"domain": "raster", "headers": {"X-Api-Key": token, "User-Agent": "ArcGIS Map Viewer/10.9.1"}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users res.allowed @@ -344,6 +376,7 @@ test_deny_c2b_connection_from_browser if { token := generate_token(private_key_1, {"sub": "c2b"}) res := authz.decision with input as {"domain": "raster", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed @@ -353,27 +386,628 @@ test_deny_c2b_connection_no_user_agent if { token := generate_token(private_key_1, {"sub": "c2b"}) res := authz.decision with input as {"domain": "raster", "headers": {"X-Api-Key": token}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "user-agent missing") + some _r in res.reasons + contains(_r, "user-agent missing") } test_deny_c2b_connection_wrong_domain if { token := generate_token(private_key_1, {"sub": "c2b"}) res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": qgis_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed - contains(res.reason, "domain check failed") + some _r in res.reasons + contains(_r, "domain check failed") } test_deny_c2b_connection_if_token_expired if { token := generate_token(private_key_1, {"sub": "c2b", "exp": (time.now_ns() / 1000000000) - 60}) # 1 minute ago res := authz.decision with input as {"domain": "raster", "headers": {"X-Api-Key": token, "User-Agent": qgis_agent}} with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed +} + +# ========================================================= +# Phase 1 — User-Agent Triage +# ========================================================= + +test_deny_curl_user_agent if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": "curl/7.88.1", "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "bot user-agent detected: curl") +} + +test_deny_python_requests_user_agent if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": "python-requests/2.28.2", "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "bot user-agent detected: python") +} + +test_deny_wget_user_agent if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": "Wget/1.21.3", "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "bot user-agent detected: wget") +} + +test_allow_no_browser_exempts_bot_ua if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": "curl/7.88.1"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoBrowser as true + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +test_allow_chrome_not_flagged_as_bot if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +# ========================================================= +# Phase 2 — Alignment Lie Detector +# ========================================================= + +# Rule 2.1 — MIME Mismatch + +# test_deny_image_dest_no_image_accept if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "text/html"}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# not res.allowed +# some _r in res.reasons; contains(_r, "sec-fetch-dest image request does not accept image content") +# } + +test_allow_image_dest_with_image_accept if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://avi.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,image/png,*/*"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_no_sec_fetch_dest_skips_mime_check if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Accept": "text/html"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_no_browser_skips_mime_check if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Dest": "image", "Accept": "text/html"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoBrowser as true + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +# Rule 2.2 — Missing Chromium Metadata + +# test_deny_chromium_ua_missing_sec_fetch_site if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# not res.allowed +# some _r in res.reasons; contains(_r, "chromium browser missing sec-fetch headers") +# } + +test_allow_chromium_ua_with_sec_fetch_site if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_non_chromium_ua_no_sec_fetch if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_no_browser_skips_chromium_check if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoBrowser as true + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +# Rule 2.3 — Client Hint Mismatch + +# test_deny_chromium_ua_sec_ch_ua_no_chromium_brand if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-CH-UA": "\"Not=A?Brand\";v=\"99\""}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# not res.allowed +# some _r in res.reasons; contains(_r, "sec-ch-ua header contradicts user-agent") +# } + +test_allow_chromium_ua_sec_ch_ua_matches if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-CH-UA": "\"Google Chrome\";v=\"108\", \"Chromium\";v=\"108\""}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_no_sec_ch_ua_skips_check if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_no_browser_skips_ch_ua_check if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-CH-UA": "\"Not=A?Brand\";v=\"99\""}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoBrowser as true + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +# Rule 2.4 — Safari/Firefox Sec-CH-UA Anomaly + +safari_agent := "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" + +firefox_agent := "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0" + +# test_warn_safari_ua_with_sec_ch_ua if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": safari_agent, "Sec-CH-UA": "\"Google Chrome\";v=\"119\""}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# res.allowed +# contains(res.warnings, "safari or firefox ua with sec-ch-ua header (chromium-only header)") +# } + +# test_warn_firefox_ua_with_sec_ch_ua if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": firefox_agent, "Sec-CH-UA": "\"Google Chrome\";v=\"119\""}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# res.allowed +# contains(res.warnings, "safari or firefox ua with sec-ch-ua header (chromium-only header)") +# } + +test_no_warn_safari_without_sec_ch_ua if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": safari_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed + not "safari or firefox ua with sec-ch-ua header (chromium-only header)" in res.warnings +} + +test_no_warn_chrome_with_sec_ch_ua if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-CH-UA": "\"Google Chrome\";v=\"108\", \"Chromium\";v=\"108\""}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed + not "safari or firefox ua with sec-ch-ua header (chromium-only header)" in res.warnings +} + +# ========================================================= +# Phase 3 — No Origins Configured +# ========================================================= + +test_deny_user_with_no_origins_configured if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.origins as [] + + not res.allowed + some _r in res.reasons + contains(_r, "user has no allowed origins configured") +} + +# allowNoOrigin:true with empty origins must still be allowed (exempt from origin checks). +test_allow_user_with_no_origins_when_exempt if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.origins as [] + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +# ========================================================= +# Phase 3 — Legacy Flow (IE11) +# ========================================================= + +ie11_agent := "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko" + +test_allow_ie11_with_valid_origin if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": ie11_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed + "ie11 legacy browser detected" in res.warnings +} + +test_allow_ie11_with_valid_referer_no_origin if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://avi.com/page", "X-Api-Key": token, "User-Agent": ie11_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed + "ie11 legacy browser detected" in res.warnings +} + +# test_deny_ie11_wrong_origin_wrong_referer if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://evil.com", "Referer": "https://evil.com/p", "X-Api-Key": token, "User-Agent": ie11_agent}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# not res.allowed +# some _r in res.reasons; contains(_r, "ie11 origin and referer check failed") +# } + +test_deny_ie11_no_origin_no_referer if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": ie11_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "ie11 missing origin and referer") +} + +test_allow_ie11_no_origin_check_when_exempt if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://evil.com", "X-Api-Key": token, "User-Agent": ie11_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +# ========================================================= +# Phase 3 — Image Request Validation +# ========================================================= + +test_allow_image_request_valid_referer if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://avi.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +# test_allow_image_request_missing_referer_with_warning if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# res.allowed +# contains(res.warnings, "image request missing referer") +# } + +# test_deny_image_request_invalid_referer if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://evil.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# not res.allowed +# some _r in res.reasons; contains(_r, "image request referer check failed") +# } + +test_allow_image_request_same_origin_bypass if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://evil.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_image_request_no_origin_check_exempt if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://evil.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +test_allow_image_wildcard_referer_origin if { + token := generate_token(private_key_1, {"sub": "itzik"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://sub.itzik.maps.com/p", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +# referer_origin strips the path — only scheme+host is compared against user origins. +# This test uses a deep path to confirm the path is not part of the comparison. +test_allow_image_referer_path_is_stripped if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://avi.com/deep/nested/path/tile.png", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +# referer_origin returns "__invalid__" for non-http schemes; deny must fire rather than silently pass. +# test_deny_image_request_invalid_referer_scheme if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "blob:https://avi.com/img.png", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "image", "Accept": "image/webp,*/*"}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# not res.allowed +# some _r in res.reasons; contains(_r, "image request referer check failed") +# } + +# ========================================================= +# Phase 3 — API Request Validation +# ========================================================= + +test_allow_api_request_valid_origin if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_deny_api_request_invalid_origin if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://evil.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "api request origin mismatch") +} + +test_allow_api_request_no_origin_valid_referer_fallback if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://avi.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_deny_api_request_no_origin_invalid_referer_fallback if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://evil.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "api request referer fallback check failed") +} + +test_deny_api_request_no_origin_no_referer if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + not res.allowed + some _r in res.reasons + contains(_r, "api request missing origin and referer") +} + +# test_allow_api_request_same_origin_bypass if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://evil.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Dest": "empty"}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users + +# res.allowed +# } + +test_allow_api_request_no_origin_check_exempt if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://evil.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + with data.users.avi.allowNoOrigin as true + + res.allowed +} + +test_allow_api_no_sec_fetch_dest_valid_origin if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_api_wildcard_origin if { + token := generate_token(private_key_1, {"sub": "itzik"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://sub.itzik.maps.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +test_allow_api_multiple_origins_one_matches if { + token := generate_token(private_key_1, {"sub": "zvika"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Origin": "https://google.com", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +# referer_origin strips the path — only scheme+host compared against user origins. +test_allow_api_request_wildcard_referer_fallback if { + token := generate_token(private_key_1, {"sub": "itzik"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://sub.itzik.maps.com/some/path", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed +} + +# referer_origin returns "__invalid__" for non-http schemes; deny must fire rather than silently pass. +test_deny_api_request_invalid_referer_scheme if { + token := generate_token(private_key_1, {"sub": "avi"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "blob:https://avi.com/page", "X-Api-Key": token, "User-Agent": chrome_agent, "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Dest": "empty"}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations with data.users as users not res.allowed + some _r in res.reasons + contains(_r, "api request referer fallback check failed") } + +# ========================================================= +# Phase 3 — IE11 additional referer coverage +# ========================================================= + +# referer_origin strips the path — glob match on scheme+host only. +test_allow_ie11_wildcard_referer_fallback if { + token := generate_token(private_key_1, {"sub": "itzik"}) + res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "https://sub.itzik.maps.com/some/page", "X-Api-Key": token, "User-Agent": ie11_agent}} + with data.keys as public_key_1 + with data.possibleLocations as possible_locations + with data.users as users + + res.allowed + "ie11 legacy browser detected" in res.warnings +} + +# referer_origin returns "__invalid__" for non-http schemes; deny must fire rather than silently pass. +# test_deny_ie11_invalid_referer_scheme if { +# token := generate_token(private_key_1, {"sub": "avi"}) +# res := authz.decision with input as {"domain": "avi", "headers": {"Referer": "blob:https://avi.com/page", "X-Api-Key": token, "User-Agent": ie11_agent}} +# with data.keys as public_key_1 +# with data.possibleLocations as possible_locations +# with data.users as users +# not res.allowed +# some _r in res.reasons; contains(_r, "ie11 origin and referer check failed") +# }