Join identity events to a trace through the user id
Kong does not propagate the trace when its openid-connect plugin calls
Keycloak: verified by tracing a request end to end and watching the
TOKEN_EXCHANGE it triggered land under Keycloak's own trace id instead.
It is not a missing setting — Kong's propagation module injects through
kong.service.request.*, which is the proxied upstream request, while the
plugin reaches Keycloak with its own HTTP client. Nothing in the
opentelemetry schema or kong_defaults.lua covers that path.
There is a join key even so: the audit record's
authenticated_entity.id is the same value as Keycloak's userId. So add a
panel listing the identities Kong authenticated on the selected trace —
usually more than one, LibreChat's own on the llm hop and the end user's
on the domain hops — and let a click on one scope the Keycloak panel to
that user.
A Grafana query variable cannot derive this automatically: Loki's
label/values endpoint rejects a pipeline ("only label matchers are
supported"), so the extracted field cannot feed a variable. Hence the
click.
That gives the intended chain for one user: LOGIN, CODE_TO_TOKEN,
USER_INFO_REQUEST, then a TOKEN_EXCHANGE per audience — verified against
live data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,17 @@
|
|||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
"description": "Set by clicking a Trace ID above, or paste one."
|
"description": "Set by clicking a Trace ID above, or paste one."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"label": "User ID",
|
||||||
|
"type": "textbox",
|
||||||
|
"query": "",
|
||||||
|
"current": {
|
||||||
|
"text": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"description": "Set by clicking an identity in the panel below. Empty means the Keycloak panel is scoped only by time."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -192,14 +203,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Keycloak identity events",
|
"title": "Identities authenticated in this trace",
|
||||||
"description": "Parsed from Keycloak's own event log: the token exchanges, logins and code-to-token swaps, with the audience minted, the requesting client, the end user and the grant type. This is where the OIDC substance lives — the Authorization header itself is REDACTED in the Kong audit records, so claims cannot be read from there.\n\nScoped to the dashboard TIME RANGE, not to the selected trace, and deliberately: Kong's openid-connect plugin calls Keycloak without propagating the trace, so those token exchanges carry their own trace id. Scoping them to the transaction would show nothing. LibreChat's own calls to Keycloak DO propagate and do appear under the transaction's trace.",
|
"description": "Who Kong authenticated at each hop of the selected trace, read from the audit records' authenticated_entity.id. A transaction usually carries more than one: LibreChat's own identity on the llm hop, the end user's on the domain hops.\n\nClick a User ID to scope the Keycloak panel below to it. That click is the join Kong does not make for us — its openid-connect plugin calls Keycloak through its own HTTP client, which Kong's propagation module does not touch, so those token exchanges carry their own trace id. The user id is the same on both sides, which is what makes the join possible.",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "loki",
|
"type": "loki",
|
||||||
"uid": "loki"
|
"uid": "loki"
|
||||||
},
|
},
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 11,
|
"h": 8,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 33
|
"y": 33
|
||||||
@@ -213,7 +224,120 @@
|
|||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"queryType": "range",
|
"queryType": "range",
|
||||||
"expr": "{job=\"keycloak/keycloak\"} |= \"org.keycloak.events\" | regexp `type=\\\\\"(?P<event>[^\\\\\"]*)\\\\\"` | regexp `clientId=\\\\\"(?P<client>[^\\\\\"]*)\\\\\"` | regexp `audience=\\\\\"(?P<audience>[^\\\\\"]*)\\\\\"` | regexp `username=\\\\\"(?P<user>[^\\\\\"]*)\\\\\"` | regexp `grant_type=\\\\\"(?P<grant>[^\\\\\"]*)\\\\\"` | regexp `scope=\\\\\"(?P<scope>[^\\\\\"]*)\\\\\"`"
|
"expr": "{job=\"kong-audit\"} |= \"$traceId\" | json authuser=\"authenticated_entity.id\", svc=\"service.name\", uri=\"request.uri\", status=\"response.status\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "extractFields",
|
||||||
|
"options": {
|
||||||
|
"source": "labels",
|
||||||
|
"replace": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "organize",
|
||||||
|
"options": {
|
||||||
|
"excludeByName": {
|
||||||
|
"labels": true,
|
||||||
|
"Line": true,
|
||||||
|
"tsNs": true,
|
||||||
|
"id": true
|
||||||
|
},
|
||||||
|
"indexByName": {
|
||||||
|
"Time": 0,
|
||||||
|
"authuser": 1,
|
||||||
|
"svc": 2,
|
||||||
|
"uri": 3,
|
||||||
|
"status": 4
|
||||||
|
},
|
||||||
|
"renameByName": {
|
||||||
|
"authuser": "User ID",
|
||||||
|
"svc": "Gateway service",
|
||||||
|
"uri": "Path",
|
||||||
|
"status": "Status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "groupBy",
|
||||||
|
"options": {
|
||||||
|
"fields": {
|
||||||
|
"User ID": {
|
||||||
|
"aggregations": [],
|
||||||
|
"operation": "groupby"
|
||||||
|
},
|
||||||
|
"Gateway service": {
|
||||||
|
"aggregations": [],
|
||||||
|
"operation": "groupby"
|
||||||
|
},
|
||||||
|
"Path": {
|
||||||
|
"aggregations": [],
|
||||||
|
"operation": "groupby"
|
||||||
|
},
|
||||||
|
"Status": {
|
||||||
|
"aggregations": [],
|
||||||
|
"operation": "groupby"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"filterable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "User ID"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "links",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"title": "Scope the Keycloak panel to this identity",
|
||||||
|
"url": "d/obs-distributed-traces/distributed-traces?${__url_time_range}&var-traceId=${traceId}&var-userId=${__value.raw}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"title": "Keycloak identity events",
|
||||||
|
"description": "Parsed from Keycloak's own event log: token exchanges, logins and code-to-token swaps, with the audience minted, the requesting client, the end user and the grant type. This is where the OIDC substance lives — the Authorization header is REDACTED in the Kong audit records, so claims cannot be read from there, and the raw token is deliberately not logged anywhere.\n\nScoped by USER, not by trace, because Kong does not propagate the trace when its openid-connect plugin calls Keycloak — verified: a request traced end to end produced a TOKEN_EXCHANGE that Keycloak recorded under its own trace id. Click a User ID in the panel above to fill it in; with it empty this shows every identity event in the time range.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "loki"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 11,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 41
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "loki"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"queryType": "range",
|
||||||
|
"expr": "{job=\"keycloak/keycloak\"} |= \"org.keycloak.events\" |= \"$userId\" | regexp `type=\\\\\"(?P<event>[^\\\\\"]*)\\\\\"` | regexp `clientId=\\\\\"(?P<client>[^\\\\\"]*)\\\\\"` | regexp `audience=\\\\\"(?P<audience>[^\\\\\"]*)\\\\\"` | regexp `username=\\\\\"(?P<user>[^\\\\\"]*)\\\\\"` | regexp `grant_type=\\\\\"(?P<grant>[^\\\\\"]*)\\\\\"` | regexp `scope=\\\\\"(?P<scope>[^\\\\\"]*)\\\\\"`"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"transformations": [
|
"transformations": [
|
||||||
@@ -280,7 +404,7 @@
|
|||||||
"h": 1,
|
"h": 1,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 44
|
"y": 52
|
||||||
},
|
},
|
||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"panels": []
|
"panels": []
|
||||||
@@ -297,7 +421,7 @@
|
|||||||
"h": 14,
|
"h": 14,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 45
|
"y": 53
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user