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.
- mobius_tracer_py-1.0.0/PKG-INFO +181 -0
- mobius_tracer_py-1.0.0/README.md +158 -0
- mobius_tracer_py-1.0.0/pyproject.toml +36 -0
- mobius_tracer_py-1.0.0/setup.cfg +4 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/__init__.py +53 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/banner.py +16 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/bootstrap.py +71 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/constants.py +55 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/context.py +84 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/errors.py +42 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/middleware.py +214 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/outgoing.py +150 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/py.typed +0 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/resources.py +139 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer/token_parser.py +86 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer_py.egg-info/PKG-INFO +181 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer_py.egg-info/SOURCES.txt +19 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer_py.egg-info/dependency_links.txt +1 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer_py.egg-info/requires.txt +19 -0
- mobius_tracer_py-1.0.0/src/mobius_tracer_py.egg-info/top_level.txt +1 -0
- mobius_tracer_py-1.0.0/tests/test_tracer.py +168 -0
|
@@ -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,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"
|