policyengine-observability 0.2.0__py3-none-any.whl
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.
- policyengine_observability/__init__.py +166 -0
- policyengine_observability/adapters/__init__.py +1 -0
- policyengine_observability/adapters/fastapi.py +286 -0
- policyengine_observability/adapters/flask.py +132 -0
- policyengine_observability/config.py +164 -0
- policyengine_observability/context.py +238 -0
- policyengine_observability/integrations/__init__.py +1 -0
- policyengine_observability/integrations/httpx.py +8 -0
- policyengine_observability/logging.py +17 -0
- policyengine_observability/runtime.py +1784 -0
- policyengine_observability/segments.py +35 -0
- policyengine_observability-0.2.0.dist-info/METADATA +52 -0
- policyengine_observability-0.2.0.dist-info/RECORD +14 -0
- policyengine_observability-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .config import ObservabilityConfig
|
|
6
|
+
from .context import OperationObservabilityContext, RequestObservabilityContext
|
|
7
|
+
from .runtime import (
|
|
8
|
+
OBSERVABILITY_INTERNAL_DISPATCH_HEADER,
|
|
9
|
+
REQUEST_ID_HEADER,
|
|
10
|
+
TRACEPARENT_HEADER,
|
|
11
|
+
ObservabilityRuntime,
|
|
12
|
+
observability_runtime,
|
|
13
|
+
set_observability_runtime,
|
|
14
|
+
)
|
|
15
|
+
from .segments import UNKNOWN_SEGMENT, coerce_segment_name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def current_context() -> RequestObservabilityContext | None:
|
|
19
|
+
return observability_runtime().current_context()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def current_operation() -> OperationObservabilityContext | None:
|
|
23
|
+
return observability_runtime().current_operation()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_attribute(key: str, value: Any) -> None:
|
|
27
|
+
observability_runtime().set_attribute(key, value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def record_error(
|
|
31
|
+
exc: BaseException,
|
|
32
|
+
*,
|
|
33
|
+
handled: bool,
|
|
34
|
+
status_code: int | None = None,
|
|
35
|
+
include_stack: bool = True,
|
|
36
|
+
) -> None:
|
|
37
|
+
observability_runtime().record_error(
|
|
38
|
+
exc,
|
|
39
|
+
handled=handled,
|
|
40
|
+
status_code=status_code,
|
|
41
|
+
include_stack=include_stack,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def record_event(event: str, **fields: Any) -> None:
|
|
46
|
+
observability_runtime().record_event(event, **fields)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def traceparent_header() -> str | None:
|
|
50
|
+
return observability_runtime().traceparent_header()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def capture_context():
|
|
54
|
+
return observability_runtime().capture_context()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def mark(key: str, ms: float) -> None:
|
|
58
|
+
observability_runtime().mark(key, ms)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def mark_ttft(key: str = "ttft_ms") -> None:
|
|
62
|
+
observability_runtime().mark_ttft(key)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def start_scope(
|
|
66
|
+
timings: dict[str, float],
|
|
67
|
+
*,
|
|
68
|
+
name: str = "operation",
|
|
69
|
+
parent_context: Any = None,
|
|
70
|
+
**attrs: Any,
|
|
71
|
+
):
|
|
72
|
+
return observability_runtime().start_scope(
|
|
73
|
+
timings,
|
|
74
|
+
name=name,
|
|
75
|
+
parent_context=parent_context,
|
|
76
|
+
**attrs,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def annotate(handle=None, **attrs: Any) -> None:
|
|
81
|
+
observability_runtime().annotate(handle, **attrs)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def end_scope(handle, error: BaseException | None = None) -> None:
|
|
85
|
+
observability_runtime().end_scope(handle, error)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def instrument_fastapi(app: Any) -> None:
|
|
89
|
+
observability_runtime().instrument_fastapi(app)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def instrument_httpx() -> None:
|
|
93
|
+
observability_runtime().instrument_httpx()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def shutdown_observability() -> None:
|
|
97
|
+
observability_runtime().shutdown()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def shutdown_tracing() -> None:
|
|
101
|
+
shutdown_observability()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def operation(name: str, *, flavor: str | None = None, **attrs: Any):
|
|
105
|
+
return observability_runtime().operation(name, flavor=flavor, **attrs)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def entrypoint(
|
|
109
|
+
name: str | None = None,
|
|
110
|
+
*,
|
|
111
|
+
flavor: str | None = None,
|
|
112
|
+
**attrs: Any,
|
|
113
|
+
):
|
|
114
|
+
return observability_runtime().entrypoint(
|
|
115
|
+
name,
|
|
116
|
+
flavor=flavor,
|
|
117
|
+
**attrs,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def segment(name: Any, **attrs: Any):
|
|
122
|
+
return observability_runtime().segment(name, **attrs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def asegment(name: Any, **attrs: Any):
|
|
126
|
+
return observability_runtime().asegment(name, **attrs)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def collect_timings(name: str = "operation", **attrs: Any):
|
|
130
|
+
return observability_runtime().collect_timings(name, **attrs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"OBSERVABILITY_INTERNAL_DISPATCH_HEADER",
|
|
135
|
+
"REQUEST_ID_HEADER",
|
|
136
|
+
"TRACEPARENT_HEADER",
|
|
137
|
+
"UNKNOWN_SEGMENT",
|
|
138
|
+
"OperationObservabilityContext",
|
|
139
|
+
"ObservabilityConfig",
|
|
140
|
+
"ObservabilityRuntime",
|
|
141
|
+
"RequestObservabilityContext",
|
|
142
|
+
"annotate",
|
|
143
|
+
"asegment",
|
|
144
|
+
"capture_context",
|
|
145
|
+
"coerce_segment_name",
|
|
146
|
+
"collect_timings",
|
|
147
|
+
"current_context",
|
|
148
|
+
"current_operation",
|
|
149
|
+
"end_scope",
|
|
150
|
+
"entrypoint",
|
|
151
|
+
"instrument_fastapi",
|
|
152
|
+
"instrument_httpx",
|
|
153
|
+
"mark",
|
|
154
|
+
"mark_ttft",
|
|
155
|
+
"observability_runtime",
|
|
156
|
+
"operation",
|
|
157
|
+
"record_error",
|
|
158
|
+
"record_event",
|
|
159
|
+
"segment",
|
|
160
|
+
"set_attribute",
|
|
161
|
+
"set_observability_runtime",
|
|
162
|
+
"shutdown_observability",
|
|
163
|
+
"shutdown_tracing",
|
|
164
|
+
"start_scope",
|
|
165
|
+
"traceparent_header",
|
|
166
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Framework adapters for policyengine-observability."""
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import parse_qs
|
|
6
|
+
|
|
7
|
+
from ..config import ObservabilityConfig
|
|
8
|
+
from ..context import RequestObservabilityContext
|
|
9
|
+
from ..runtime import (
|
|
10
|
+
REQUEST_ID_HEADER,
|
|
11
|
+
TRACEPARENT_HEADER,
|
|
12
|
+
ObservabilityRuntime,
|
|
13
|
+
set_observability_runtime,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
UNMATCHED_ROUTE = "<unmatched>"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FastAPIObservabilityAdapter:
|
|
20
|
+
def __init__(self, runtime: ObservabilityRuntime) -> None:
|
|
21
|
+
self.runtime = runtime
|
|
22
|
+
|
|
23
|
+
def instrument_app(self, app: Any) -> None:
|
|
24
|
+
if not self.runtime.enabled:
|
|
25
|
+
return
|
|
26
|
+
if getattr(app.state, "policyengine_observability_adapter", None):
|
|
27
|
+
return
|
|
28
|
+
app.state.policyengine_observability_adapter = self
|
|
29
|
+
if self.runtime.config.instrument_fastapi:
|
|
30
|
+
self.runtime.instrument_fastapi(app)
|
|
31
|
+
try:
|
|
32
|
+
app.add_middleware(
|
|
33
|
+
FastAPIObservabilityMiddleware,
|
|
34
|
+
adapter=self,
|
|
35
|
+
)
|
|
36
|
+
except BaseException as exc:
|
|
37
|
+
self.runtime.log_observability_failure(
|
|
38
|
+
"fastapi.middleware_install",
|
|
39
|
+
exc,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def start_request(self, scope: dict[str, Any]) -> None:
|
|
43
|
+
try:
|
|
44
|
+
headers = _headers_from_scope(scope)
|
|
45
|
+
path = scope.get("path") or ""
|
|
46
|
+
route = _route_from_scope(scope) or UNMATCHED_ROUTE
|
|
47
|
+
endpoint = _endpoint_from_scope(scope)
|
|
48
|
+
request_id = headers.get(REQUEST_ID_HEADER.lower()) or str(
|
|
49
|
+
uuid.uuid4()
|
|
50
|
+
)
|
|
51
|
+
context = RequestObservabilityContext(
|
|
52
|
+
config=self.runtime.config,
|
|
53
|
+
request_id=request_id,
|
|
54
|
+
method=scope.get("method") or "",
|
|
55
|
+
route=route,
|
|
56
|
+
path=path,
|
|
57
|
+
endpoint=endpoint,
|
|
58
|
+
query_keys=_query_keys(scope),
|
|
59
|
+
content_length_bytes=_int_header(
|
|
60
|
+
headers.get("content-length")
|
|
61
|
+
),
|
|
62
|
+
inbound=self._inbound_metadata(scope, headers),
|
|
63
|
+
)
|
|
64
|
+
self.runtime.begin_request(context, carrier=headers)
|
|
65
|
+
except BaseException as exc:
|
|
66
|
+
self.runtime.log_observability_failure(
|
|
67
|
+
"fastapi.before_request",
|
|
68
|
+
exc,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def update_resolved_route(self, scope: dict[str, Any]) -> None:
|
|
72
|
+
route = _route_from_scope(scope)
|
|
73
|
+
endpoint = _endpoint_from_scope(scope)
|
|
74
|
+
if route or endpoint:
|
|
75
|
+
self.runtime.update_request_route(route=route, endpoint=endpoint)
|
|
76
|
+
|
|
77
|
+
def _inbound_metadata(
|
|
78
|
+
self,
|
|
79
|
+
scope: dict[str, Any],
|
|
80
|
+
headers: dict[str, str],
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
forwarded_for = _split_forwarded_for(headers.get("x-forwarded-for"))
|
|
83
|
+
x_real_ip = headers.get("x-real-ip")
|
|
84
|
+
client = scope.get("client") or ()
|
|
85
|
+
remote_addr = client[0] if client else None
|
|
86
|
+
client_ip = None
|
|
87
|
+
ip_source = None
|
|
88
|
+
if forwarded_for:
|
|
89
|
+
client_ip = forwarded_for[0]
|
|
90
|
+
ip_source = "x_forwarded_for"
|
|
91
|
+
elif x_real_ip:
|
|
92
|
+
client_ip = x_real_ip
|
|
93
|
+
ip_source = "x_real_ip"
|
|
94
|
+
elif remote_addr:
|
|
95
|
+
client_ip = remote_addr
|
|
96
|
+
ip_source = "remote_addr"
|
|
97
|
+
metadata = {
|
|
98
|
+
"ip_source": ip_source,
|
|
99
|
+
"user_agent": headers.get("user-agent"),
|
|
100
|
+
"origin": headers.get("origin"),
|
|
101
|
+
"referer": headers.get("referer"),
|
|
102
|
+
"host": headers.get("host"),
|
|
103
|
+
"content_length_bytes": _int_header(headers.get("content-length")),
|
|
104
|
+
}
|
|
105
|
+
if self.runtime.config.log_raw_ip:
|
|
106
|
+
metadata["client_ip"] = client_ip
|
|
107
|
+
metadata["forwarded_for"] = forwarded_for
|
|
108
|
+
metadata["x_real_ip"] = x_real_ip
|
|
109
|
+
return metadata
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class FastAPIObservabilityMiddleware:
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
app: Any,
|
|
116
|
+
*,
|
|
117
|
+
adapter: FastAPIObservabilityAdapter,
|
|
118
|
+
) -> None:
|
|
119
|
+
self.app = app
|
|
120
|
+
self.adapter = adapter
|
|
121
|
+
|
|
122
|
+
async def __call__(self, scope, receive, send) -> None:
|
|
123
|
+
if scope.get("type") != "http":
|
|
124
|
+
await self.app(scope, receive, send)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
self.adapter.start_request(scope)
|
|
128
|
+
status_code: int | None = None
|
|
129
|
+
completed = False
|
|
130
|
+
|
|
131
|
+
async def send_wrapper(message) -> None:
|
|
132
|
+
nonlocal completed
|
|
133
|
+
nonlocal status_code
|
|
134
|
+
|
|
135
|
+
if message["type"] == "http.response.start":
|
|
136
|
+
status_code = int(message.get("status") or 0)
|
|
137
|
+
self.adapter.update_resolved_route(scope)
|
|
138
|
+
response_headers = self.adapter.runtime.prepare_response(
|
|
139
|
+
status_code
|
|
140
|
+
)
|
|
141
|
+
if response_headers:
|
|
142
|
+
message = {
|
|
143
|
+
**message,
|
|
144
|
+
"headers": _merge_response_headers(
|
|
145
|
+
message.get("headers") or [],
|
|
146
|
+
response_headers,
|
|
147
|
+
),
|
|
148
|
+
}
|
|
149
|
+
await send(message)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if message["type"] == "http.response.body" and not message.get(
|
|
153
|
+
"more_body", False
|
|
154
|
+
):
|
|
155
|
+
try:
|
|
156
|
+
await send(message)
|
|
157
|
+
finally:
|
|
158
|
+
completed = True
|
|
159
|
+
self.adapter.update_resolved_route(scope)
|
|
160
|
+
self.adapter.runtime.complete_request(status_code)
|
|
161
|
+
self.adapter.runtime.teardown_request(None)
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
await send(message)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
await self.app(scope, receive, send_wrapper)
|
|
168
|
+
except BaseException as exc:
|
|
169
|
+
if not completed:
|
|
170
|
+
error_status = status_code or 500
|
|
171
|
+
self.adapter.update_resolved_route(scope)
|
|
172
|
+
self.adapter.runtime.prepare_response(error_status)
|
|
173
|
+
self.adapter.runtime.complete_request(error_status)
|
|
174
|
+
self.adapter.runtime.teardown_request(exc)
|
|
175
|
+
completed = True
|
|
176
|
+
raise
|
|
177
|
+
finally:
|
|
178
|
+
if not completed:
|
|
179
|
+
self.adapter.update_resolved_route(scope)
|
|
180
|
+
self.adapter.runtime.complete_request(status_code)
|
|
181
|
+
self.adapter.runtime.teardown_request(None)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def init_fastapi_observability(
|
|
185
|
+
app: Any,
|
|
186
|
+
*,
|
|
187
|
+
config: ObservabilityConfig | None = None,
|
|
188
|
+
runtime: ObservabilityRuntime | None = None,
|
|
189
|
+
service_name: str,
|
|
190
|
+
service_role: str = "api",
|
|
191
|
+
span_prefix: str | None = None,
|
|
192
|
+
segment_registry=None,
|
|
193
|
+
) -> ObservabilityRuntime:
|
|
194
|
+
existing = getattr(app.state, "policyengine_observability", None)
|
|
195
|
+
if existing:
|
|
196
|
+
return existing
|
|
197
|
+
runtime = runtime or ObservabilityRuntime(
|
|
198
|
+
config
|
|
199
|
+
or ObservabilityConfig.from_env(
|
|
200
|
+
service_name=service_name,
|
|
201
|
+
service_role=service_role,
|
|
202
|
+
span_prefix=span_prefix,
|
|
203
|
+
),
|
|
204
|
+
segment_registry=segment_registry,
|
|
205
|
+
)
|
|
206
|
+
runtime.configure()
|
|
207
|
+
app.state.policyengine_observability = runtime
|
|
208
|
+
set_observability_runtime(runtime)
|
|
209
|
+
FastAPIObservabilityAdapter(runtime).instrument_app(app)
|
|
210
|
+
return runtime
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _headers_from_scope(scope: dict[str, Any]) -> dict[str, str]:
|
|
214
|
+
headers: dict[str, str] = {}
|
|
215
|
+
for key, value in scope.get("headers") or []:
|
|
216
|
+
try:
|
|
217
|
+
header_key = key.decode("latin-1").lower()
|
|
218
|
+
header_value = value.decode("latin-1")
|
|
219
|
+
except BaseException:
|
|
220
|
+
continue
|
|
221
|
+
if header_key in headers:
|
|
222
|
+
headers[header_key] = f"{headers[header_key]},{header_value}"
|
|
223
|
+
else:
|
|
224
|
+
headers[header_key] = header_value
|
|
225
|
+
return headers
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _route_from_scope(scope: dict[str, Any]) -> str | None:
|
|
229
|
+
route = scope.get("route")
|
|
230
|
+
route_path = getattr(route, "path", None)
|
|
231
|
+
if route_path:
|
|
232
|
+
return str(route_path)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _endpoint_from_scope(scope: dict[str, Any]) -> str | None:
|
|
237
|
+
endpoint = scope.get("endpoint")
|
|
238
|
+
if endpoint is None:
|
|
239
|
+
return None
|
|
240
|
+
endpoint_name = getattr(endpoint, "__name__", None)
|
|
241
|
+
return endpoint_name or str(endpoint)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _query_keys(scope: dict[str, Any]) -> list[str]:
|
|
245
|
+
query_string = scope.get("query_string") or b""
|
|
246
|
+
try:
|
|
247
|
+
decoded = query_string.decode("latin-1")
|
|
248
|
+
except AttributeError:
|
|
249
|
+
decoded = str(query_string)
|
|
250
|
+
return sorted(parse_qs(decoded, keep_blank_values=True).keys())
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _merge_response_headers(
|
|
254
|
+
existing_headers: list[tuple[bytes, bytes]],
|
|
255
|
+
headers: dict[str, str],
|
|
256
|
+
) -> list[tuple[bytes, bytes]]:
|
|
257
|
+
response_header_names = {key.lower().encode("latin-1") for key in headers}
|
|
258
|
+
merged = [
|
|
259
|
+
(key, value)
|
|
260
|
+
for key, value in existing_headers
|
|
261
|
+
if key.lower() not in response_header_names
|
|
262
|
+
]
|
|
263
|
+
merged.extend(
|
|
264
|
+
(
|
|
265
|
+
key.encode("latin-1"),
|
|
266
|
+
value.encode("latin-1"),
|
|
267
|
+
)
|
|
268
|
+
for key, value in headers.items()
|
|
269
|
+
if key in {REQUEST_ID_HEADER, TRACEPARENT_HEADER}
|
|
270
|
+
)
|
|
271
|
+
return merged
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _split_forwarded_for(value: str | None) -> list[str]:
|
|
275
|
+
if not value:
|
|
276
|
+
return []
|
|
277
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _int_header(value: str | None) -> int | None:
|
|
281
|
+
if value is None:
|
|
282
|
+
return None
|
|
283
|
+
try:
|
|
284
|
+
return int(value)
|
|
285
|
+
except ValueError:
|
|
286
|
+
return None
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..config import ObservabilityConfig
|
|
7
|
+
from ..context import RequestObservabilityContext
|
|
8
|
+
from ..runtime import (
|
|
9
|
+
OBSERVABILITY_INTERNAL_DISPATCH_HEADER,
|
|
10
|
+
REQUEST_ID_HEADER,
|
|
11
|
+
ObservabilityRuntime,
|
|
12
|
+
set_observability_runtime,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FlaskObservabilityAdapter:
|
|
17
|
+
def __init__(self, runtime: ObservabilityRuntime) -> None:
|
|
18
|
+
self.runtime = runtime
|
|
19
|
+
|
|
20
|
+
def instrument_app(self, app: Any) -> None:
|
|
21
|
+
if not self.runtime.enabled:
|
|
22
|
+
return
|
|
23
|
+
if app.extensions.get("policyengine_observability_adapter"):
|
|
24
|
+
return
|
|
25
|
+
app.extensions["policyengine_observability_adapter"] = self
|
|
26
|
+
|
|
27
|
+
@app.before_request
|
|
28
|
+
def _start_observed_request() -> None:
|
|
29
|
+
self.start_request()
|
|
30
|
+
|
|
31
|
+
@app.after_request
|
|
32
|
+
def _finish_observed_request(response):
|
|
33
|
+
headers = self.runtime.finish_request(response.status_code)
|
|
34
|
+
for key, value in headers.items():
|
|
35
|
+
response.headers[key] = value
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
@app.teardown_request
|
|
39
|
+
def _emit_observed_request(exc) -> None:
|
|
40
|
+
self.runtime.teardown_request(exc)
|
|
41
|
+
|
|
42
|
+
def start_request(self) -> None:
|
|
43
|
+
try:
|
|
44
|
+
from flask import request
|
|
45
|
+
|
|
46
|
+
route = request.url_rule.rule if request.url_rule else request.path
|
|
47
|
+
request_id = request.headers.get(REQUEST_ID_HEADER) or str(
|
|
48
|
+
uuid.uuid4()
|
|
49
|
+
)
|
|
50
|
+
context = RequestObservabilityContext(
|
|
51
|
+
config=self.runtime.config,
|
|
52
|
+
request_id=request_id,
|
|
53
|
+
method=request.method,
|
|
54
|
+
route=route,
|
|
55
|
+
path=request.path,
|
|
56
|
+
endpoint=request.endpoint,
|
|
57
|
+
query_keys=sorted(request.args.keys()),
|
|
58
|
+
content_length_bytes=request.content_length,
|
|
59
|
+
inbound=self._inbound_metadata(request),
|
|
60
|
+
internal_dispatch=(
|
|
61
|
+
request.headers.get(OBSERVABILITY_INTERNAL_DISPATCH_HEADER)
|
|
62
|
+
== "1"
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
self.runtime.begin_request(context, carrier=request.headers)
|
|
66
|
+
except BaseException as exc:
|
|
67
|
+
self.runtime.log_observability_failure("flask.before_request", exc)
|
|
68
|
+
|
|
69
|
+
def _inbound_metadata(self, request) -> dict:
|
|
70
|
+
forwarded_for = _split_forwarded_for(
|
|
71
|
+
request.headers.get("X-Forwarded-For")
|
|
72
|
+
)
|
|
73
|
+
x_real_ip = request.headers.get("X-Real-IP")
|
|
74
|
+
remote_addr = request.remote_addr
|
|
75
|
+
client_ip = None
|
|
76
|
+
ip_source = None
|
|
77
|
+
if forwarded_for:
|
|
78
|
+
client_ip = forwarded_for[0]
|
|
79
|
+
ip_source = "x_forwarded_for"
|
|
80
|
+
elif x_real_ip:
|
|
81
|
+
client_ip = x_real_ip
|
|
82
|
+
ip_source = "x_real_ip"
|
|
83
|
+
elif remote_addr:
|
|
84
|
+
client_ip = remote_addr
|
|
85
|
+
ip_source = "remote_addr"
|
|
86
|
+
metadata = {
|
|
87
|
+
"ip_source": ip_source,
|
|
88
|
+
"user_agent": request.headers.get("User-Agent"),
|
|
89
|
+
"origin": request.headers.get("Origin"),
|
|
90
|
+
"referer": request.headers.get("Referer"),
|
|
91
|
+
"host": request.host,
|
|
92
|
+
"content_length_bytes": request.content_length,
|
|
93
|
+
}
|
|
94
|
+
if self.runtime.config.log_raw_ip:
|
|
95
|
+
metadata["client_ip"] = client_ip
|
|
96
|
+
metadata["forwarded_for"] = forwarded_for
|
|
97
|
+
metadata["x_real_ip"] = x_real_ip
|
|
98
|
+
return metadata
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def init_flask_observability(
|
|
102
|
+
app: Any,
|
|
103
|
+
*,
|
|
104
|
+
config: ObservabilityConfig | None = None,
|
|
105
|
+
runtime: ObservabilityRuntime | None = None,
|
|
106
|
+
service_name: str,
|
|
107
|
+
service_role: str = "api",
|
|
108
|
+
span_prefix: str | None = None,
|
|
109
|
+
segment_registry=None,
|
|
110
|
+
) -> ObservabilityRuntime:
|
|
111
|
+
if app.extensions.get("policyengine_observability"):
|
|
112
|
+
return app.extensions["policyengine_observability"]
|
|
113
|
+
runtime = runtime or ObservabilityRuntime(
|
|
114
|
+
config
|
|
115
|
+
or ObservabilityConfig.from_env(
|
|
116
|
+
service_name=service_name,
|
|
117
|
+
service_role=service_role,
|
|
118
|
+
span_prefix=span_prefix,
|
|
119
|
+
),
|
|
120
|
+
segment_registry=segment_registry,
|
|
121
|
+
)
|
|
122
|
+
runtime.configure()
|
|
123
|
+
app.extensions["policyengine_observability"] = runtime
|
|
124
|
+
set_observability_runtime(runtime)
|
|
125
|
+
FlaskObservabilityAdapter(runtime).instrument_app(app)
|
|
126
|
+
return runtime
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _split_forwarded_for(value: str | None) -> list[str]:
|
|
130
|
+
if not value:
|
|
131
|
+
return []
|
|
132
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|