mobius-tracer-py 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: mobius-tracer-py
3
+ Version: 1.0.0
4
+ Summary: Request-context propagation, JWT parsing, security headers and OpenTelemetry enrichment for Mobius Python (FastAPI) services.
5
+ Author: Mobius Platform
6
+ Keywords: tracing,context-propagation,fastapi,mobius,opentelemetry
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: starlette>=0.27
10
+ Provides-Extra: otel
11
+ Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
12
+ Provides-Extra: httpx
13
+ Requires-Dist: httpx>=0.24; extra == "httpx"
14
+ Provides-Extra: resources
15
+ Requires-Dist: psutil>=5.9; extra == "resources"
16
+ Provides-Extra: banner
17
+ Requires-Dist: pyfiglet>=1.0; extra == "banner"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.4; extra == "dev"
20
+ Requires-Dist: starlette>=0.27; extra == "dev"
21
+ Requires-Dist: httpx>=0.24; extra == "dev"
22
+ Requires-Dist: opentelemetry-api>=1.20; extra == "dev"
23
+
24
+ # mobius-tracer-py
25
+
26
+ Request-context propagation for Mobius **Python (FastAPI / Starlette)** services.
27
+
28
+ On every incoming request it extracts identity/trace headers and the JWT
29
+ payload, exposes them request-scoped, forwards them onto downstream calls, sets
30
+ security + trace response headers, and enriches OpenTelemetry spans.
31
+
32
+ - **PyPI:** `pip install mobius-tracer-py`
33
+ - **Import package:** `mobius_tracer`
34
+ - **Python:** 3.10+
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install mobius-tracer-py
40
+
41
+ # optional features
42
+ pip install "mobius-tracer-py[otel]" # set OTel span attributes
43
+ pip install "mobius-tracer-py[httpx]" # TracedClient / httpx hook
44
+ pip install "mobius-tracer-py[resources]" # CPU/mem span metrics (psutil)
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from fastapi import FastAPI
51
+ from mobius_tracer import setup_tracer, current
52
+
53
+ app = FastAPI()
54
+ setup_tracer(app) # adds middleware + 401 handler for invalid tokens
55
+
56
+ @app.get("/me")
57
+ async def me():
58
+ ctx = current()
59
+ return {"tenant_id": ctx.tenant_id, "txn": ctx.req_transaction_id}
60
+ ```
61
+
62
+ `setup_tracer` installs a middleware that, per request:
63
+
64
+ 1. extracts `x-request-id`, `x-b3-traceid`, `x-b3-spanid`, `Authorization`,
65
+ `appId`, `platformId`, and `X-Transaction-Id` (generated if absent);
66
+ 2. parses the JWT payload into the request context (tenant/agent/user/email …);
67
+ 3. derives `action_log_user_id` from the token;
68
+ 4. sets security response headers + `x-mb-trace-id` + `X-Transaction-Id`;
69
+ 5. sets OpenTelemetry span attributes (`txn.id`, `tenant.id`, `user.id`).
70
+
71
+ ### Options
72
+
73
+ ```python
74
+ setup_tracer(
75
+ app,
76
+ skip_paths=("actuator", "health", "metrics", "error", "prometheus",
77
+ "swagger", "api-docs", "arazzo"), # paths with no token check
78
+ require_token=True, # 401 on protected paths without a valid token
79
+ set_security_headers=True, # HSTS/CSP/X-Frame-Options/...
80
+ propagate_to_otel=True, # txn/tenant/user span attributes
81
+ integrate_logging=True, # mirror fields into mobius-logging-py context
82
+ measure_resources=True, # CPU/mem of each request onto the span
83
+ instrument_httpx=False, # patch httpx so all outbound calls propagate (opt-in)
84
+ )
85
+ ```
86
+
87
+ ## Request context
88
+
89
+ ```python
90
+ from mobius_tracer import current, get, HeadersHolder
91
+
92
+ ctx = current() # HeadersHolder for this request
93
+ ctx.tenant_id # e.g. "tenant-123"
94
+ ctx.action_log_user_id # derived user id
95
+ get("trace_id") # field accessor
96
+ ```
97
+
98
+ Fields: `request_id`, `trace_id`, `span_id`, `tenant_id`, `tenant_user_id`,
99
+ `consumer_id`, `email`, `authorization`, `name`, `requester_type`, `platform_id`,
100
+ `parent_tenant_id`, `app_id`, `action_log_user_id`, `agent_id`, `agent_user_id`,
101
+ `req_transaction_id`, `b_agent_id`.
102
+
103
+ The context uses `contextvars`, so it is isolated per request and correct across
104
+ `async` tasks and threads.
105
+
106
+ ## Propagating to downstream calls
107
+
108
+ Forward the current context (and a W3C `traceparent`) to outbound requests:
109
+
110
+ ```python
111
+ import httpx
112
+ from mobius_tracer import traced_headers, httpx_auth_hook, TracedClient
113
+
114
+ # 1) build headers yourself
115
+ httpx.get(url, headers=traced_headers())
116
+
117
+ # 2) an httpx event hook
118
+ client = httpx.AsyncClient(event_hooks={"request": [httpx_auth_hook()]})
119
+
120
+ # 3) a ready-made traced client
121
+ with TracedClient(base_url="https://api.internal") as c:
122
+ c.get("/orders/1")
123
+
124
+ # 4) globally patch httpx so ALL outbound calls propagate (internal-only!)
125
+ from mobius_tracer import instrument_httpx
126
+ instrument_httpx() # or setup_tracer(app, instrument_httpx=True)
127
+ ```
128
+
129
+ Forwarded headers: `x-request-id`, `x-b3-traceid`, `x-b3-spanid`, `tenantId`,
130
+ `tenantUserId`, `consumerId`, `email`, `name`, `Authorization`, `platformId`,
131
+ `parentTenantId`, `appId`, `X-Transaction-Id`, plus `traceparent`.
132
+
133
+ ## JWT parsing
134
+
135
+ ```python
136
+ from mobius_tracer import parse_token, TokenError
137
+
138
+ body = parse_token(jwt_string) # decodes payload (no signature verification)
139
+ body.tenant_id, body.requester_type, body.email
140
+ ```
141
+
142
+ Raises `TokenError` (HTTP 401) for missing or malformed tokens. A non-empty
143
+ `requester_type` claim is required.
144
+
145
+ ## Resource metrics
146
+
147
+ With `measure_resources=True` (default), **every request** is measured and the
148
+ metrics are attached to its OpenTelemetry span automatically. For a specific
149
+ function or block:
150
+
151
+ ```python
152
+ from mobius_tracer import measure_resources, measure
153
+
154
+ @measure_resources
155
+ async def heavy(): ...
156
+
157
+ with measure():
158
+ do_work()
159
+ ```
160
+
161
+ Span attributes: `wall_time_ms`, `cpu_used_ms`, `cpu_used_pct`, `cpu_cores`,
162
+ `mem_rss_mb`, `mem_rss_delta_mb`, `mem_system_used_pct`. Best-effort; never
163
+ raises. Uses `psutil` when installed.
164
+
165
+ ## Integrating with mobius-logging-py
166
+
167
+ ```python
168
+ setup_tracer(app, integrate_logging=True)
169
+ ```
170
+
171
+ When `mobius-logging-py` is installed, the middleware mirrors `tenant_id`,
172
+ `trace_id`, `txn_id`, `agent_id`, and `correlation_id` into its logging context,
173
+ so every log line carries the same identity fields.
174
+
175
+ ## Development
176
+
177
+ ```bash
178
+ python -m venv .venv && source .venv/bin/activate
179
+ pip install -e ".[dev]"
180
+ pytest
181
+ ```
@@ -0,0 +1,158 @@
1
+ # mobius-tracer-py
2
+
3
+ Request-context propagation for Mobius **Python (FastAPI / Starlette)** services.
4
+
5
+ On every incoming request it extracts identity/trace headers and the JWT
6
+ payload, exposes them request-scoped, forwards them onto downstream calls, sets
7
+ security + trace response headers, and enriches OpenTelemetry spans.
8
+
9
+ - **PyPI:** `pip install mobius-tracer-py`
10
+ - **Import package:** `mobius_tracer`
11
+ - **Python:** 3.10+
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install mobius-tracer-py
17
+
18
+ # optional features
19
+ pip install "mobius-tracer-py[otel]" # set OTel span attributes
20
+ pip install "mobius-tracer-py[httpx]" # TracedClient / httpx hook
21
+ pip install "mobius-tracer-py[resources]" # CPU/mem span metrics (psutil)
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ from fastapi import FastAPI
28
+ from mobius_tracer import setup_tracer, current
29
+
30
+ app = FastAPI()
31
+ setup_tracer(app) # adds middleware + 401 handler for invalid tokens
32
+
33
+ @app.get("/me")
34
+ async def me():
35
+ ctx = current()
36
+ return {"tenant_id": ctx.tenant_id, "txn": ctx.req_transaction_id}
37
+ ```
38
+
39
+ `setup_tracer` installs a middleware that, per request:
40
+
41
+ 1. extracts `x-request-id`, `x-b3-traceid`, `x-b3-spanid`, `Authorization`,
42
+ `appId`, `platformId`, and `X-Transaction-Id` (generated if absent);
43
+ 2. parses the JWT payload into the request context (tenant/agent/user/email …);
44
+ 3. derives `action_log_user_id` from the token;
45
+ 4. sets security response headers + `x-mb-trace-id` + `X-Transaction-Id`;
46
+ 5. sets OpenTelemetry span attributes (`txn.id`, `tenant.id`, `user.id`).
47
+
48
+ ### Options
49
+
50
+ ```python
51
+ setup_tracer(
52
+ app,
53
+ skip_paths=("actuator", "health", "metrics", "error", "prometheus",
54
+ "swagger", "api-docs", "arazzo"), # paths with no token check
55
+ require_token=True, # 401 on protected paths without a valid token
56
+ set_security_headers=True, # HSTS/CSP/X-Frame-Options/...
57
+ propagate_to_otel=True, # txn/tenant/user span attributes
58
+ integrate_logging=True, # mirror fields into mobius-logging-py context
59
+ measure_resources=True, # CPU/mem of each request onto the span
60
+ instrument_httpx=False, # patch httpx so all outbound calls propagate (opt-in)
61
+ )
62
+ ```
63
+
64
+ ## Request context
65
+
66
+ ```python
67
+ from mobius_tracer import current, get, HeadersHolder
68
+
69
+ ctx = current() # HeadersHolder for this request
70
+ ctx.tenant_id # e.g. "tenant-123"
71
+ ctx.action_log_user_id # derived user id
72
+ get("trace_id") # field accessor
73
+ ```
74
+
75
+ Fields: `request_id`, `trace_id`, `span_id`, `tenant_id`, `tenant_user_id`,
76
+ `consumer_id`, `email`, `authorization`, `name`, `requester_type`, `platform_id`,
77
+ `parent_tenant_id`, `app_id`, `action_log_user_id`, `agent_id`, `agent_user_id`,
78
+ `req_transaction_id`, `b_agent_id`.
79
+
80
+ The context uses `contextvars`, so it is isolated per request and correct across
81
+ `async` tasks and threads.
82
+
83
+ ## Propagating to downstream calls
84
+
85
+ Forward the current context (and a W3C `traceparent`) to outbound requests:
86
+
87
+ ```python
88
+ import httpx
89
+ from mobius_tracer import traced_headers, httpx_auth_hook, TracedClient
90
+
91
+ # 1) build headers yourself
92
+ httpx.get(url, headers=traced_headers())
93
+
94
+ # 2) an httpx event hook
95
+ client = httpx.AsyncClient(event_hooks={"request": [httpx_auth_hook()]})
96
+
97
+ # 3) a ready-made traced client
98
+ with TracedClient(base_url="https://api.internal") as c:
99
+ c.get("/orders/1")
100
+
101
+ # 4) globally patch httpx so ALL outbound calls propagate (internal-only!)
102
+ from mobius_tracer import instrument_httpx
103
+ instrument_httpx() # or setup_tracer(app, instrument_httpx=True)
104
+ ```
105
+
106
+ Forwarded headers: `x-request-id`, `x-b3-traceid`, `x-b3-spanid`, `tenantId`,
107
+ `tenantUserId`, `consumerId`, `email`, `name`, `Authorization`, `platformId`,
108
+ `parentTenantId`, `appId`, `X-Transaction-Id`, plus `traceparent`.
109
+
110
+ ## JWT parsing
111
+
112
+ ```python
113
+ from mobius_tracer import parse_token, TokenError
114
+
115
+ body = parse_token(jwt_string) # decodes payload (no signature verification)
116
+ body.tenant_id, body.requester_type, body.email
117
+ ```
118
+
119
+ Raises `TokenError` (HTTP 401) for missing or malformed tokens. A non-empty
120
+ `requester_type` claim is required.
121
+
122
+ ## Resource metrics
123
+
124
+ With `measure_resources=True` (default), **every request** is measured and the
125
+ metrics are attached to its OpenTelemetry span automatically. For a specific
126
+ function or block:
127
+
128
+ ```python
129
+ from mobius_tracer import measure_resources, measure
130
+
131
+ @measure_resources
132
+ async def heavy(): ...
133
+
134
+ with measure():
135
+ do_work()
136
+ ```
137
+
138
+ Span attributes: `wall_time_ms`, `cpu_used_ms`, `cpu_used_pct`, `cpu_cores`,
139
+ `mem_rss_mb`, `mem_rss_delta_mb`, `mem_system_used_pct`. Best-effort; never
140
+ raises. Uses `psutil` when installed.
141
+
142
+ ## Integrating with mobius-logging-py
143
+
144
+ ```python
145
+ setup_tracer(app, integrate_logging=True)
146
+ ```
147
+
148
+ When `mobius-logging-py` is installed, the middleware mirrors `tenant_id`,
149
+ `trace_id`, `txn_id`, `agent_id`, and `correlation_id` into its logging context,
150
+ so every log line carries the same identity fields.
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ python -m venv .venv && source .venv/bin/activate
156
+ pip install -e ".[dev]"
157
+ pytest
158
+ ```
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mobius-tracer-py"
7
+ version = "1.0.0"
8
+ description = "Request-context propagation, JWT parsing, security headers and OpenTelemetry enrichment for Mobius Python (FastAPI) services."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "Mobius Platform" }]
12
+ keywords = ["tracing", "context-propagation", "fastapi", "mobius", "opentelemetry"]
13
+ dependencies = [
14
+ "starlette>=0.27",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ otel = ["opentelemetry-api>=1.20"]
19
+ httpx = ["httpx>=0.24"]
20
+ resources = ["psutil>=5.9"]
21
+ banner = ["pyfiglet>=1.0"]
22
+ dev = [
23
+ "pytest>=7.4",
24
+ "starlette>=0.27",
25
+ "httpx>=0.24",
26
+ "opentelemetry-api>=1.20",
27
+ ]
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
31
+
32
+ [tool.setuptools.package-data]
33
+ mobius_tracer = ["py.typed"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,53 @@
1
+ """Mobius Tracer for Python.
2
+
3
+ Request-context propagation for Mobius FastAPI services: extract identity/trace
4
+ headers and the JWT payload from incoming requests, expose them request-scoped,
5
+ forward them onto downstream calls, set security + trace response headers, and
6
+ enrich OpenTelemetry spans.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from . import constants, context
11
+ from .banner import print_banner
12
+ from .bootstrap import setup_tracer
13
+ from .context import HeadersHolder, clear, current, get, set_holder
14
+ from .errors import TokenError
15
+ from .middleware import IncomingRequestsMiddleware
16
+ from .outgoing import (
17
+ TracedClient,
18
+ generate_traceparent,
19
+ httpx_auth_hook,
20
+ instrument_httpx,
21
+ outgoing_headers,
22
+ traced_headers,
23
+ uninstrument_httpx,
24
+ )
25
+ from .resources import measure, measure_resources
26
+ from .token_parser import TokenBody, parse_token
27
+
28
+ __version__ = "1.0.0"
29
+
30
+ __all__ = [
31
+ "constants",
32
+ "context",
33
+ "setup_tracer",
34
+ "IncomingRequestsMiddleware",
35
+ "HeadersHolder",
36
+ "current",
37
+ "get",
38
+ "set_holder",
39
+ "clear",
40
+ "TokenError",
41
+ "TokenBody",
42
+ "parse_token",
43
+ "outgoing_headers",
44
+ "traced_headers",
45
+ "generate_traceparent",
46
+ "httpx_auth_hook",
47
+ "instrument_httpx",
48
+ "uninstrument_httpx",
49
+ "TracedClient",
50
+ "measure",
51
+ "measure_resources",
52
+ "print_banner",
53
+ ]
@@ -0,0 +1,16 @@
1
+ """Optional startup ASCII banner."""
2
+ from __future__ import annotations
3
+
4
+
5
+ def print_banner(app_name: str = "APPLICATION", version: str = "1.0") -> None:
6
+ name = app_name.upper()
7
+ try:
8
+ import pyfiglet # type: ignore
9
+
10
+ art = pyfiglet.figlet_format(name)
11
+ except Exception: # noqa: BLE001 - pyfiglet optional / any render failure
12
+ art = f":: {name} ::"
13
+ cyan, yellow, reset = "\033[36m", "\033[33m", "\033[0m"
14
+ print(f"{cyan}{art}{reset}")
15
+ print(f"{yellow} :: {name} :: v{version}{reset}")
16
+ print("-" * 80)
@@ -0,0 +1,71 @@
1
+ """One-call setup for FastAPI services.
2
+
3
+ Registers the incoming-request middleware and a handler that turns
4
+ :class:`TokenError` into a JSON 401/4xx response.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Iterable
9
+
10
+ from . import constants
11
+ from .errors import TokenError
12
+ from .middleware import IncomingRequestsMiddleware
13
+
14
+
15
+ def setup_tracer(
16
+ app: Any,
17
+ *,
18
+ skip_paths: Iterable[str] = constants.DEFAULT_SKIP_PATHS,
19
+ require_token: bool = True,
20
+ set_security_headers: bool = True,
21
+ propagate_to_otel: bool = True,
22
+ integrate_logging: bool = True,
23
+ measure_resources: bool = True,
24
+ instrument_httpx: bool = False,
25
+ register_error_handler: bool = True,
26
+ ) -> None:
27
+ """Wire the tracer into a FastAPI/Starlette ``app``.
28
+
29
+ Per request the middleware extracts identity/trace context, sets security +
30
+ trace response headers, enriches the OpenTelemetry span, and:
31
+
32
+ - ``integrate_logging`` (default on): mirror identity/trace fields into the
33
+ mobius-logging-py context when that package is installed (a no-op
34
+ otherwise), so every log line carries them.
35
+ - ``measure_resources`` (default on): record CPU/memory of each request onto
36
+ the active span.
37
+ - ``instrument_httpx``: globally patch httpx so ALL outbound calls propagate
38
+ the context. Off by default - it forwards auth/tenant headers to every
39
+ httpx call, so only enable it when all outbound calls are internal. For
40
+ targeted propagation use ``TracedClient`` / ``httpx_auth_hook`` instead.
41
+ """
42
+ app.add_middleware(
43
+ IncomingRequestsMiddleware,
44
+ skip_paths=skip_paths,
45
+ require_token=require_token,
46
+ set_security_headers=set_security_headers,
47
+ propagate_to_otel=propagate_to_otel,
48
+ integrate_logging=integrate_logging,
49
+ measure_resources=measure_resources,
50
+ )
51
+
52
+ if instrument_httpx:
53
+ from .outgoing import instrument_httpx as _instrument
54
+
55
+ _instrument()
56
+
57
+ if register_error_handler:
58
+ _register_error_handler(app)
59
+
60
+
61
+ def _register_error_handler(app: Any) -> None:
62
+ try:
63
+ from starlette.responses import JSONResponse
64
+ except ImportError: # pragma: no cover
65
+ return
66
+
67
+ async def _token_error_handler(_request, exc: TokenError): # noqa: ANN001
68
+ return JSONResponse(status_code=exc.status_code, content=exc.to_response())
69
+
70
+ # add_exception_handler works on both Starlette and FastAPI apps.
71
+ app.add_exception_handler(TokenError, _token_error_handler)
@@ -0,0 +1,55 @@
1
+ """Header names and security-header values.
2
+
3
+ Defines the wire headers Mobius services propagate so identity and trace
4
+ context stays consistent across the platform.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ # --- incoming / outgoing identity + trace headers ---
9
+ BAGENT_ID_HEADER = "bAgentId"
10
+ TX_ID_HEADER = "X-Transaction-Id"
11
+ MB_TRACE_RESPONSE_HEADER = "x-mb-trace-id"
12
+ HEADER_REQUEST_ID = "x-request-id"
13
+ HEADER_TRACE_ID = "x-b3-traceid"
14
+ HEADER_SPAN_ID = "x-b3-spanid"
15
+ REQUESTER_TYPE = "RequesterType"
16
+
17
+ HEADER_TOKEN = "token"
18
+ HEADER_AUTHORIZATION = "Authorization"
19
+ HEADER_TENANT_ID = "tenantId"
20
+ HEADER_TENANT_USERID = "tenantUserId"
21
+ HEADER_CONSUMERID = "consumerId"
22
+ HEADER_EMAIL = "email"
23
+ HEADER_NAME = "name"
24
+ HEADER_PLATFORMID = "platformId"
25
+ HEADER_PARENT_TENANTID = "parentTenantId"
26
+ HEADER_APPID = "appId"
27
+ HEADER_ACTIONLOG_USERID = "actionLogUserId"
28
+ HEADER_AGENT_TENANT_ID = "agentId"
29
+ HEADER_AGENT_USER_ID = "agentUserId"
30
+ HEADER_SERVICE_ID = "serviceId"
31
+
32
+ JWT_PREFIX = "Bearer"
33
+
34
+ # --- security response headers ---
35
+ SECURITY_HEADERS = {
36
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
37
+ "Content-Security-Policy": "default-src 'self'",
38
+ "X-Content-Type-Options": "nosniff",
39
+ "X-Frame-Options": "DENY",
40
+ "X-XSS-Protection": "1; mode=block",
41
+ "Permissions-Policy": "no-referrer",
42
+ "Referrer-Policy": "geolocation=(self)",
43
+ }
44
+
45
+ # Default paths excluded from token validation / context extraction.
46
+ DEFAULT_SKIP_PATHS = (
47
+ "actuator",
48
+ "health",
49
+ "metrics",
50
+ "error",
51
+ "prometheus",
52
+ "swagger",
53
+ "api-docs",
54
+ "arazzo",
55
+ )
@@ -0,0 +1,84 @@
1
+ """Request-scoped header context.
2
+
3
+ Holds the identity/trace fields extracted from an incoming request. Backed by a
4
+ :class:`contextvars.ContextVar` so each request/task has isolated state across
5
+ threads and ``asyncio`` tasks.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import contextvars
10
+ from dataclasses import asdict, dataclass, fields
11
+ from typing import Dict, Optional
12
+
13
+ # Field name -> outbound header name (used by the outgoing propagator).
14
+ OUTGOING_HEADER_MAP = {
15
+ "request_id": "x-request-id",
16
+ "trace_id": "x-b3-traceid",
17
+ "span_id": "x-b3-spanid",
18
+ "tenant_id": "tenantId",
19
+ "tenant_user_id": "tenantUserId",
20
+ "consumer_id": "consumerId",
21
+ "email": "email",
22
+ "name": "name",
23
+ "authorization": "Authorization",
24
+ "platform_id": "platformId",
25
+ "parent_tenant_id": "parentTenantId",
26
+ "app_id": "appId",
27
+ "req_transaction_id": "X-Transaction-Id",
28
+ }
29
+
30
+
31
+ @dataclass
32
+ class HeadersHolder:
33
+ request_id: Optional[str] = None
34
+ trace_id: Optional[str] = None
35
+ span_id: Optional[str] = None
36
+ tenant_id: Optional[str] = None
37
+ tenant_user_id: Optional[str] = None
38
+ consumer_id: Optional[str] = None
39
+ email: Optional[str] = None
40
+ authorization: Optional[str] = None
41
+ name: Optional[str] = None
42
+ requester_type: Optional[str] = None
43
+ platform_id: Optional[str] = None
44
+ parent_tenant_id: Optional[str] = None
45
+ app_id: Optional[str] = None
46
+ action_log_user_id: Optional[str] = None
47
+ agent_id: Optional[str] = None
48
+ agent_user_id: Optional[str] = None
49
+ req_transaction_id: Optional[str] = None
50
+ b_agent_id: Optional[str] = None
51
+
52
+ def as_dict(self) -> Dict[str, Optional[str]]:
53
+ return asdict(self)
54
+
55
+
56
+ _holder: contextvars.ContextVar[Optional[HeadersHolder]] = contextvars.ContextVar(
57
+ "mobius_tracer_headers", default=None
58
+ )
59
+
60
+
61
+ def set_holder(holder: HeadersHolder) -> None:
62
+ _holder.set(holder)
63
+
64
+
65
+ def current() -> HeadersHolder:
66
+ """Return the holder for the current request (empty one if unset)."""
67
+ holder = _holder.get()
68
+ if holder is None:
69
+ holder = HeadersHolder()
70
+ _holder.set(holder)
71
+ return holder
72
+
73
+
74
+ def get(field_name: str) -> Optional[str]:
75
+ return getattr(current(), field_name, None)
76
+
77
+
78
+ def clear() -> None:
79
+ _holder.set(HeadersHolder())
80
+
81
+
82
+ # Validate the outgoing map references real fields at import time.
83
+ _VALID = {f.name for f in fields(HeadersHolder)}
84
+ assert set(OUTGOING_HEADER_MAP).issubset(_VALID), "OUTGOING_HEADER_MAP has unknown fields"