Tag traced requests with the session id and OIDC sub
This commit is contained in:
@@ -52,6 +52,7 @@ from .telemetry import (
|
||||
CountingInMemorySessionStore,
|
||||
CountingRedisSessionStore,
|
||||
otel_headers,
|
||||
session_tag_hook,
|
||||
telemetry_from_settings,
|
||||
)
|
||||
|
||||
@@ -111,6 +112,10 @@ def otel_mixin_from_settings(
|
||||
"endpoint": settings.otel_exporter_endpoint,
|
||||
"headers": otel_headers(settings),
|
||||
"excluded_paths": settings.otel_excluded_paths,
|
||||
# Runs after the session mixin's after hook, so the session id has
|
||||
# been persisted/minted and the OIDC claims are on the session.
|
||||
"server_response_hook": session_tag_hook,
|
||||
"websocket_close_hook": session_tag_hook,
|
||||
}
|
||||
if meter_provider is not None:
|
||||
kwargs["meter_provider"] = meter_provider
|
||||
|
||||
@@ -12,7 +12,10 @@ the test suite never need them):
|
||||
CPU time/utilization, memory usage and CPython garbage-collection
|
||||
counters;
|
||||
* OTLP log export through a stdlib :class:`logging.Handler` attached to
|
||||
the root logger, mirroring what the console sees.
|
||||
the root logger, mirroring what the console sees;
|
||||
* session/user tagging of request and websocket spans through
|
||||
:func:`session_tag_hook` (kaya-otel's response/close hooks), so traces
|
||||
can be correlated with a session (hashed id) and an OIDC user.
|
||||
|
||||
The store counts are asynchronous while OpenTelemetry instrument
|
||||
callbacks are synchronous, so :class:`TelemetryMixin` polls the stores on
|
||||
@@ -21,10 +24,11 @@ a background task and its callbacks report the cached values.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from logging import getLogger
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Union
|
||||
|
||||
from kaya.core import KayaApp, KayaMixin
|
||||
from kaya.session import InMemorySessionStore, SessionStore
|
||||
@@ -79,6 +83,38 @@ def _signal_endpoint(base: Optional[str], signal: str) -> Optional[str]:
|
||||
return f"{base}/{signal}" if base else None
|
||||
|
||||
|
||||
def session_tag_hook(span: Any, carrier: Any) -> None:
|
||||
"""Tag an HTTP request/websocket span with the request's session/user.
|
||||
|
||||
Wired into kaya-otel as its ``server_response_hook`` and
|
||||
``websocket_close_hook``: those run after the session mixin's after
|
||||
hook has persisted (and, at login, minted) the session, so the final
|
||||
session id is visible here.
|
||||
|
||||
The session id is the session cookie, i.e. a credential, so only a
|
||||
truncated SHA-256 of it is exported; session ids are
|
||||
``secrets.token_urlsafe(32)`` (256 bits), which makes the digest
|
||||
irreversible in practice. The OIDC ``sub`` is a pseudonymous
|
||||
identifier already persisted in Postgres and served by the API, so it
|
||||
is exported verbatim as ``user.id`` (OpenTelemetry's current end-user
|
||||
attribute; ``enduser.id`` is PII-flagged and being superseded).
|
||||
"""
|
||||
session = getattr(carrier, "session", None)
|
||||
if session is None:
|
||||
return
|
||||
session_id = getattr(session, "id", None)
|
||||
if session_id:
|
||||
span.set_attribute(
|
||||
"session.id",
|
||||
hashlib.sha256(session_id.encode()).hexdigest()[:16],
|
||||
)
|
||||
user = session.get("oidc_user")
|
||||
if isinstance(user, Mapping):
|
||||
sub = user.get("sub")
|
||||
if sub:
|
||||
span.set_attribute("user.id", str(sub))
|
||||
|
||||
|
||||
class _NoOtelLogs(logging.Filter):
|
||||
"""Drop OpenTelemetry's own log records.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user