{ "uid": "obs-access-logs", "title": "Access Logs", "description": "One row per HTTP request, across the Kong gateways, Keycloak, the Classic Models API and the ingress controller. Each source is parsed by its own format and normalised to the same columns.", "tags": [ "observability", "logs", "access" ], "editable": false, "schemaVersion": 39, "version": 1, "refresh": "", "time": { "from": "now-1h", "to": "now" }, "timezone": "browser", "templating": { "list": [ { "name": "source", "label": "Source", "type": "custom", "query": "Kong gateways : kong-audit,Keycloak : keycloak/keycloak,Classic Models API : classic-models/classic-models-api,Ingress NGINX : ingress-nginx/controller", "multi": true, "includeAll": true, "allValue": "kong-audit|keycloak/keycloak|classic-models/classic-models-api|ingress-nginx/controller", "current": { "text": [ "All" ], "value": [ "$__all" ] } }, { "name": "search", "label": "Search", "type": "textbox", "query": "", "current": { "text": "", "value": "" }, "description": "Case-insensitive regex over the whole line. Empty matches everything." } ] }, "panels": [ { "id": 1, "type": "stat", "title": "Requests", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum(count_over_time({job=~\"${source:regex}\"} |~ `(?i)$search` [$__range])) or vector(0)", "queryType": "instant" } ], "fieldConfig": { "defaults": { "unit": "short", "decimals": 0, "mappings": [], "color": { "mode": "fixed", "fixedColor": "blue" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] } }, "overrides": [] }, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "colorMode": "value", "graphMode": "none", "textMode": "auto" }, "description": "Every line the selected sources emitted over the dashboard's time range." }, { "id": 2, "type": "stat", "title": "Client errors (4xx)", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum(count_over_time({job=~\"${source:regex}\", job=\"kong-audit\"} |~ `(?i)$search` | json method=\"request.method\", path=\"request.uri\", status=\"response.status\", client=\"client_ip\", latency=\"latencies.request\", size=\"response.size\" | status =~ \"4..\" [$__range])) or vector(0)", "queryType": "instant" } ], "fieldConfig": { "defaults": { "unit": "short", "decimals": 0, "mappings": [], "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "text", "value": null }, { "color": "orange", "value": 1 } ] } }, "overrides": [] }, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "colorMode": "value", "graphMode": "none", "textMode": "auto" }, "description": "Counted on the Kong records only, the one source that exposes a parsed status without ambiguity. The status column of the table covers the others." }, { "id": 3, "type": "stat", "title": "Server errors (5xx)", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum(count_over_time({job=~\"${source:regex}\", job=\"kong-audit\"} |~ `(?i)$search` | json method=\"request.method\", path=\"request.uri\", status=\"response.status\", client=\"client_ip\", latency=\"latencies.request\", size=\"response.size\" | status =~ \"5..\" [$__range])) or vector(0)", "queryType": "instant" } ], "fieldConfig": { "defaults": { "unit": "short", "decimals": 0, "mappings": [], "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "text", "value": null }, { "color": "red", "value": 1 } ] } }, "overrides": [] }, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "colorMode": "value", "graphMode": "none", "textMode": "auto" } }, { "id": 4, "type": "stat", "title": "Sources active", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "count(sum by (job) (count_over_time({job=~\"${source:regex}\"} |~ `(?i)$search` [$__range]))) or vector(0)", "queryType": "instant" } ], "fieldConfig": { "defaults": { "unit": "short", "decimals": 0, "mappings": [], "color": { "mode": "fixed", "fixedColor": "purple" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "purple", "value": null } ] } }, "overrides": [] }, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "colorMode": "value", "graphMode": "none", "textMode": "auto" } }, { "id": 5, "type": "timeseries", "title": "Requests by source", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (job) (count_over_time({job=~\"${source:regex}\", job=\"kong-audit\"} |~ `(?i)$search` | json method=\"request.method\", path=\"request.uri\", status=\"response.status\", client=\"client_ip\", latency=\"latencies.request\", size=\"response.size\" [$__auto]))", "legendFormat": "{{job}}", "queryType": "range" }, { "refId": "B", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (job) (count_over_time({job=~\"${source:regex}\", job=\"keycloak/keycloak\"} |~ `(?i)$search` | json loggerName=\"loggerName\", m=\"message\" | loggerName=\"org.keycloak.http.access-log\" | line_format `{{.m}}` | pattern ` - - [<_>] \" <_>\" ` [$__auto]))", "legendFormat": "{{job}}", "queryType": "range" }, { "refId": "C", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (job) (count_over_time({job=~\"${source:regex}\", job=\"classic-models/classic-models-api\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" ` [$__auto]))", "legendFormat": "{{job}}", "queryType": "range" }, { "refId": "D", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (job) (count_over_time({job=~\"${source:regex}\", job=\"ingress-nginx/controller\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" ` [$__auto]))", "legendFormat": "{{job}}", "queryType": "range" } ], "fieldConfig": { "defaults": { "unit": "short", "min": 0, "mappings": [], "color": { "mode": "palette-classic" }, "custom": { "drawStyle": "bars", "lineWidth": 1, "fillOpacity": 80, "showPoints": "never", "barAlignment": 0, "stacking": { "mode": "normal", "group": "A" } } }, "overrides": [] }, "options": { "legend": { "showLegend": true, "displayMode": "table", "placement": "bottom", "calcs": [ "sum" ] }, "tooltip": { "mode": "multi", "sort": "desc" } }, "description": "Bars rather than lines: a request count per bucket is a run of discrete events." }, { "id": 6, "type": "timeseries", "title": "Status classes", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (class) (count_over_time({job=~\"${source:regex}\", job=\"kong-audit\"} |~ `(?i)$search` | json method=\"request.method\", path=\"request.uri\", status=\"response.status\", client=\"client_ip\", latency=\"latencies.request\", size=\"response.size\" | label_format class=`{{ printf \"%.1sxx\" .status }}` [$__auto]))", "legendFormat": "{{class}}", "queryType": "range" }, { "refId": "B", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (class) (count_over_time({job=~\"${source:regex}\", job=\"keycloak/keycloak\"} |~ `(?i)$search` | json loggerName=\"loggerName\", m=\"message\" | loggerName=\"org.keycloak.http.access-log\" | line_format `{{.m}}` | pattern ` - - [<_>] \" <_>\" ` | label_format class=`{{ printf \"%.1sxx\" .status }}` [$__auto]))", "legendFormat": "{{class}}", "queryType": "range" }, { "refId": "C", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (class) (count_over_time({job=~\"${source:regex}\", job=\"classic-models/classic-models-api\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" ` | label_format class=`{{ printf \"%.1sxx\" .status }}` [$__auto]))", "legendFormat": "{{class}}", "queryType": "range" }, { "refId": "D", "datasource": { "type": "loki", "uid": "loki" }, "expr": "sum by (class) (count_over_time({job=~\"${source:regex}\", job=\"ingress-nginx/controller\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" ` | label_format class=`{{ printf \"%.1sxx\" .status }}` [$__auto]))", "legendFormat": "{{class}}", "queryType": "range" } ], "fieldConfig": { "defaults": { "unit": "short", "min": 0, "mappings": [], "color": { "mode": "palette-classic" }, "custom": { "drawStyle": "bars", "lineWidth": 1, "fillOpacity": 80, "showPoints": "never", "barAlignment": 0, "stacking": { "mode": "normal", "group": "A" } } }, "overrides": [ { "matcher": { "id": "byName", "options": "2xx" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } } ] }, { "matcher": { "id": "byName", "options": "3xx" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } } ] }, { "matcher": { "id": "byName", "options": "4xx" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "orange" } } ] }, { "matcher": { "id": "byName", "options": "5xx" }, "properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } } ] } ] }, "options": { "legend": { "showLegend": true, "displayMode": "table", "placement": "bottom", "calcs": [ "sum" ] }, "tooltip": { "mode": "multi", "sort": "desc" } }, "description": "Derived from each source's parsed status, so the four formats end up on the same scale." }, { "id": 7, "type": "table", "title": "Requests", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 14, "w": 24, "x": 0, "y": 12 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"kong-audit\"} |~ `(?i)$search` | json method=\"request.method\", path=\"request.uri\", status=\"response.status\", client=\"client_ip\", latency=\"latencies.request\", size=\"response.size\"", "queryType": "range", "maxLines": 1000 }, { "refId": "B", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"keycloak/keycloak\"} |~ `(?i)$search` | json loggerName=\"loggerName\", m=\"message\" | loggerName=\"org.keycloak.http.access-log\" | line_format `{{.m}}` | pattern ` - - [<_>] \" <_>\" `", "queryType": "range", "maxLines": 1000 }, { "refId": "C", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"classic-models/classic-models-api\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" `", "queryType": "range", "maxLines": 1000 }, { "refId": "D", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"ingress-nginx/controller\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" `", "queryType": "range", "maxLines": 1000 } ], "transformations": [ { "id": "extractFields", "options": { "source": "labels", "replace": false } }, { "id": "merge", "options": {} }, { "id": "filterFieldsByName", "options": { "include": { "pattern": "^(timestamp|body|job|method|path|status|client|latency|size)$" } } }, { "id": "organize", "options": { "excludeByName": {}, "indexByName": { "Time": 0, "Source": 1, "Method": 2, "Path": 3, "Status": 4, "Client": 5, "Latency (ms)": 6, "Bytes": 7, "Raw line": 8 }, "renameByName": { "timestamp": "Time", "body": "Raw line", "job": "Source", "method": "Method", "path": "Path", "status": "Status", "client": "Client", "latency": "Latency (ms)", "size": "Bytes" } } } ], "fieldConfig": { "defaults": { "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "inspect": true, "filterable": true }, "mappings": [] }, "overrides": [ { "matcher": { "id": "byName", "options": "Source" }, "properties": [ { "id": "custom.width", "value": 210 } ] }, { "matcher": { "id": "byName", "options": "Method" }, "properties": [ { "id": "custom.width", "value": 80 } ] }, { "matcher": { "id": "byName", "options": "Status" }, "properties": [ { "id": "custom.width", "value": 80 }, { "id": "mappings", "value": [ { "type": "regex", "options": { "pattern": "^2..$", "result": { "color": "green", "index": 0 } } }, { "type": "regex", "options": { "pattern": "^3..$", "result": { "color": "blue", "index": 1 } } }, { "type": "regex", "options": { "pattern": "^4..$", "result": { "color": "orange", "index": 2 } } }, { "type": "regex", "options": { "pattern": "^5..$", "result": { "color": "red", "index": 3 } } } ] }, { "id": "custom.cellOptions", "value": { "type": "color-text" } } ] }, { "matcher": { "id": "byName", "options": "Client" }, "properties": [ { "id": "custom.width", "value": 130 } ] }, { "matcher": { "id": "byName", "options": "Latency (ms)" }, "properties": [ { "id": "custom.width", "value": 110 } ] }, { "matcher": { "id": "byName", "options": "Bytes" }, "properties": [ { "id": "custom.width", "value": 90 } ] }, { "matcher": { "id": "byName", "options": "Raw line" }, "properties": [ { "id": "custom.width", "value": 240 } ] } ] }, "options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false, "reducer": [ "sum" ], "countRows": false, "fields": "" }, "sortBy": [ { "displayName": "Time", "desc": true } ] }, "description": "One row per request across all four sources. Latency is populated for Kong only — the NCSA format the others use carries no timing field at all, which is a property of the format, not a gap in the query." }, { "id": 8, "type": "logs", "title": "Requests as a log stream", "datasource": { "type": "loki", "uid": "loki" }, "gridPos": { "h": 10, "w": 24, "x": 0, "y": 26 }, "targets": [ { "refId": "A", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"kong-audit\"} |~ `(?i)$search` | json method=\"request.method\", path=\"request.uri\", status=\"response.status\", client=\"client_ip\", latency=\"latencies.request\", size=\"response.size\"", "queryType": "range" }, { "refId": "B", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"keycloak/keycloak\"} |~ `(?i)$search` | json loggerName=\"loggerName\", m=\"message\" | loggerName=\"org.keycloak.http.access-log\" | line_format `{{.m}}` | pattern ` - - [<_>] \" <_>\" `", "queryType": "range" }, { "refId": "C", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"classic-models/classic-models-api\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" `", "queryType": "range" }, { "refId": "D", "datasource": { "type": "loki", "uid": "loki" }, "expr": "{job=~\"${source:regex}\", job=\"ingress-nginx/controller\"} |~ `(?i)$search` | pattern ` - - [<_>] \" <_>\" `", "queryType": "range" } ], "options": { "showTime": true, "showLabels": false, "showCommonLabels": false, "wrapLogMessage": true, "prettifyLogMessage": true, "sortOrder": "Descending", "dedupStrategy": "none", "enableLogDetails": true }, "description": "Same result set. Kept for live tail and for the trace link the Loki datasource derives from a trace id, neither of which a table can do." } ] }