logbrew-flask 0.1.1__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.
- logbrew_flask/__init__.py +402 -0
- logbrew_flask/examples/__init__.py +1 -0
- logbrew_flask/examples/__main__.py +32 -0
- logbrew_flask/examples/dependency_spans.py +139 -0
- logbrew_flask/examples/outbound_http.py +102 -0
- logbrew_flask/examples/readme_example.py +28 -0
- logbrew_flask/examples/real_user_smoke.py +74 -0
- logbrew_flask/py.typed +1 -0
- logbrew_flask-0.1.1.dist-info/METADATA +156 -0
- logbrew_flask-0.1.1.dist-info/RECORD +12 -0
- logbrew_flask-0.1.1.dist-info/WHEEL +5 -0
- logbrew_flask-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Flask integration helpers for capturing LogBrew request spans and exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from flask import Flask, Response, g, request
|
|
13
|
+
from flask.signals import got_request_exception
|
|
14
|
+
from logbrew_sdk import (
|
|
15
|
+
LogBrewClient,
|
|
16
|
+
LogBrewTraceContext,
|
|
17
|
+
MetricAttributes,
|
|
18
|
+
RecordingTransport,
|
|
19
|
+
SdkError,
|
|
20
|
+
SpanAttributes,
|
|
21
|
+
TransportError,
|
|
22
|
+
create_logbrew_trace_context,
|
|
23
|
+
get_active_logbrew_trace,
|
|
24
|
+
parse_traceparent,
|
|
25
|
+
span_attributes_from_trace_context,
|
|
26
|
+
trace_metadata,
|
|
27
|
+
use_logbrew_trace,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class LogBrewFlaskConfig:
|
|
33
|
+
"""Runtime options used by the LogBrew Flask middleware."""
|
|
34
|
+
|
|
35
|
+
client: LogBrewClient
|
|
36
|
+
transport: RecordingTransport | None = None
|
|
37
|
+
capture_successful_requests: bool = True
|
|
38
|
+
capture_request_metrics: bool = False
|
|
39
|
+
capture_exceptions: bool = True
|
|
40
|
+
flush_on_response: bool = True
|
|
41
|
+
raise_flush_errors: bool = False
|
|
42
|
+
service_name: str = "flask"
|
|
43
|
+
request_metric_name: str = "http.server.duration"
|
|
44
|
+
span_id_factory: Callable[[], str] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def utc_timestamp() -> str:
|
|
48
|
+
"""Return a LogBrew-compatible UTC timestamp."""
|
|
49
|
+
|
|
50
|
+
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def request_name() -> str:
|
|
54
|
+
"""Return the stable request name used for span and issue titles."""
|
|
55
|
+
|
|
56
|
+
return f"{request.method} {request_route_template()}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def request_metadata(
|
|
60
|
+
*,
|
|
61
|
+
status_code: int | None = None,
|
|
62
|
+
duration_ms: float | None = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""Return metadata that is useful for request-level troubleshooting without including query strings."""
|
|
65
|
+
|
|
66
|
+
route_template = request_route_template()
|
|
67
|
+
metadata: dict[str, Any] = {
|
|
68
|
+
"framework": "flask",
|
|
69
|
+
"method": request.method,
|
|
70
|
+
"routeTemplate": route_template,
|
|
71
|
+
}
|
|
72
|
+
if route_template == request.path:
|
|
73
|
+
metadata["path"] = request.path
|
|
74
|
+
rule = getattr(request.url_rule, "rule", None)
|
|
75
|
+
endpoint = getattr(request.url_rule, "endpoint", None)
|
|
76
|
+
if isinstance(rule, str):
|
|
77
|
+
metadata["route"] = rule
|
|
78
|
+
if isinstance(endpoint, str):
|
|
79
|
+
metadata["endpoint"] = endpoint
|
|
80
|
+
if status_code is not None:
|
|
81
|
+
metadata["status_code"] = status_code
|
|
82
|
+
if duration_ms is not None:
|
|
83
|
+
metadata["duration_ms"] = round(duration_ms, 3)
|
|
84
|
+
return metadata
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def request_route_template() -> str:
|
|
88
|
+
"""Return a low-cardinality Flask route template without query strings."""
|
|
89
|
+
|
|
90
|
+
rule = getattr(request.url_rule, "rule", None)
|
|
91
|
+
template = rule if isinstance(rule, str) and rule else request.path
|
|
92
|
+
return route_template_only(template)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def route_template_only(value: str) -> str:
|
|
96
|
+
"""Strip query/hash text from a route template and normalize empty values."""
|
|
97
|
+
|
|
98
|
+
route_template = value.split("?", 1)[0].split("#", 1)[0].strip()
|
|
99
|
+
return route_template or "/"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def status_code_class(status_code: int) -> str:
|
|
103
|
+
"""Return the coarse HTTP status code class used by request metrics."""
|
|
104
|
+
|
|
105
|
+
return f"{status_code // 100}xx" if 100 <= status_code <= 599 else "unknown"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_request_metric_attributes(
|
|
109
|
+
*,
|
|
110
|
+
status_code: int,
|
|
111
|
+
duration_ms: float,
|
|
112
|
+
metric_name: str = "http.server.duration",
|
|
113
|
+
) -> MetricAttributes:
|
|
114
|
+
"""Create privacy-safe request duration metric attributes for a completed Flask request."""
|
|
115
|
+
|
|
116
|
+
duration_value = float(duration_ms)
|
|
117
|
+
if duration_value < 0:
|
|
118
|
+
duration_value = 0.0
|
|
119
|
+
return {
|
|
120
|
+
"name": metric_name,
|
|
121
|
+
"kind": "histogram",
|
|
122
|
+
"value": duration_value,
|
|
123
|
+
"unit": "ms",
|
|
124
|
+
"temporality": "delta",
|
|
125
|
+
"metadata": {
|
|
126
|
+
"framework": "flask",
|
|
127
|
+
"method": request.method,
|
|
128
|
+
"routeTemplate": request_route_template(),
|
|
129
|
+
"statusCode": status_code,
|
|
130
|
+
"statusCodeClass": status_code_class(status_code),
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def capture_request_metric(
|
|
136
|
+
client: LogBrewClient,
|
|
137
|
+
*,
|
|
138
|
+
status_code: int,
|
|
139
|
+
duration_ms: float,
|
|
140
|
+
event_id: str | None = None,
|
|
141
|
+
timestamp: str | None = None,
|
|
142
|
+
metric_name: str = "http.server.duration",
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Capture a Flask request duration metric and return its event id."""
|
|
145
|
+
|
|
146
|
+
metric_event_id = event_id or f"evt_flask_metric_{uuid.uuid4().hex}"
|
|
147
|
+
client.metric(
|
|
148
|
+
metric_event_id,
|
|
149
|
+
timestamp or utc_timestamp(),
|
|
150
|
+
create_request_metric_attributes(
|
|
151
|
+
status_code=status_code,
|
|
152
|
+
duration_ms=duration_ms,
|
|
153
|
+
metric_name=metric_name,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
return metric_event_id
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def capture_request_span(
|
|
160
|
+
client: LogBrewClient,
|
|
161
|
+
*,
|
|
162
|
+
status_code: int,
|
|
163
|
+
duration_ms: float,
|
|
164
|
+
event_id: str | None = None,
|
|
165
|
+
timestamp: str | None = None,
|
|
166
|
+
span_id_factory: Callable[[], str] | None = None,
|
|
167
|
+
trace: LogBrewTraceContext | None = None,
|
|
168
|
+
) -> str:
|
|
169
|
+
"""Capture a Flask request as a LogBrew span event and return its event id."""
|
|
170
|
+
|
|
171
|
+
span_event_id = event_id or f"evt_flask_span_{uuid.uuid4().hex}"
|
|
172
|
+
trace_context = trace or request_logbrew_trace() or create_request_trace_context(
|
|
173
|
+
span_id_factory=span_id_factory,
|
|
174
|
+
)
|
|
175
|
+
attributes: SpanAttributes = span_attributes_from_trace_context(
|
|
176
|
+
trace_context,
|
|
177
|
+
name=request_name(),
|
|
178
|
+
status="ok" if status_code < 500 else "error",
|
|
179
|
+
duration_ms=duration_ms,
|
|
180
|
+
metadata=request_metadata(status_code=status_code, duration_ms=duration_ms),
|
|
181
|
+
)
|
|
182
|
+
client.span(
|
|
183
|
+
span_event_id,
|
|
184
|
+
timestamp or utc_timestamp(),
|
|
185
|
+
attributes,
|
|
186
|
+
)
|
|
187
|
+
return span_event_id
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def capture_exception(
|
|
191
|
+
client: LogBrewClient,
|
|
192
|
+
exc: BaseException,
|
|
193
|
+
*,
|
|
194
|
+
event_id: str | None = None,
|
|
195
|
+
timestamp: str | None = None,
|
|
196
|
+
trace: LogBrewTraceContext | None = None,
|
|
197
|
+
) -> str:
|
|
198
|
+
"""Capture an exception raised while handling a Flask request and return its event id."""
|
|
199
|
+
|
|
200
|
+
issue_event_id = event_id or f"evt_flask_issue_{uuid.uuid4().hex}"
|
|
201
|
+
trace_context = trace or request_logbrew_trace() or get_active_logbrew_trace()
|
|
202
|
+
client.issue(
|
|
203
|
+
issue_event_id,
|
|
204
|
+
timestamp or utc_timestamp(),
|
|
205
|
+
{
|
|
206
|
+
"title": f"{request_name()} failed",
|
|
207
|
+
"level": "error",
|
|
208
|
+
"message": str(exc) or exc.__class__.__name__,
|
|
209
|
+
"metadata": {
|
|
210
|
+
**request_metadata(status_code=500),
|
|
211
|
+
"exception_type": exc.__class__.__name__,
|
|
212
|
+
**trace_metadata(trace_context),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
return issue_event_id
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def add_logbrew_middleware(
|
|
220
|
+
app: Flask,
|
|
221
|
+
*,
|
|
222
|
+
client: LogBrewClient,
|
|
223
|
+
transport: RecordingTransport | None = None,
|
|
224
|
+
capture_successful_requests: bool = True,
|
|
225
|
+
capture_request_metrics: bool = False,
|
|
226
|
+
capture_exceptions: bool = True,
|
|
227
|
+
flush_on_response: bool = True,
|
|
228
|
+
raise_flush_errors: bool = False,
|
|
229
|
+
service_name: str = "flask",
|
|
230
|
+
request_metric_name: str = "http.server.duration",
|
|
231
|
+
span_id_factory: Callable[[], str] | None = None,
|
|
232
|
+
) -> LogBrewFlaskConfig:
|
|
233
|
+
"""Install LogBrew request/exception capture hooks on a Flask app."""
|
|
234
|
+
|
|
235
|
+
config = LogBrewFlaskConfig(
|
|
236
|
+
client=client,
|
|
237
|
+
transport=transport,
|
|
238
|
+
capture_successful_requests=capture_successful_requests,
|
|
239
|
+
capture_request_metrics=capture_request_metrics,
|
|
240
|
+
capture_exceptions=capture_exceptions,
|
|
241
|
+
flush_on_response=flush_on_response,
|
|
242
|
+
raise_flush_errors=raise_flush_errors,
|
|
243
|
+
service_name=service_name,
|
|
244
|
+
request_metric_name=request_metric_name,
|
|
245
|
+
span_id_factory=span_id_factory,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@app.before_request
|
|
249
|
+
def logbrew_before_request() -> None:
|
|
250
|
+
trace_context = create_request_trace_context(span_id_factory=config.span_id_factory)
|
|
251
|
+
g.logbrew_trace = trace_context
|
|
252
|
+
g.logbrew_started_at = time.perf_counter()
|
|
253
|
+
g.logbrew_exception_captured = False
|
|
254
|
+
g.logbrew_span_captured = False
|
|
255
|
+
activation = use_logbrew_trace(trace_context)
|
|
256
|
+
activation.__enter__()
|
|
257
|
+
g.logbrew_trace_activation = activation
|
|
258
|
+
|
|
259
|
+
@app.after_request
|
|
260
|
+
def logbrew_after_request(response: Response) -> Response:
|
|
261
|
+
duration_ms = request_duration_ms()
|
|
262
|
+
exception_captured = bool(getattr(g, "logbrew_exception_captured", False))
|
|
263
|
+
should_capture_request_span = (
|
|
264
|
+
not exception_captured and (config.capture_successful_requests or response.status_code >= 500)
|
|
265
|
+
)
|
|
266
|
+
if should_capture_request_span:
|
|
267
|
+
capture_request_span(
|
|
268
|
+
config.client,
|
|
269
|
+
status_code=response.status_code,
|
|
270
|
+
duration_ms=duration_ms,
|
|
271
|
+
span_id_factory=config.span_id_factory,
|
|
272
|
+
trace=request_logbrew_trace(),
|
|
273
|
+
)
|
|
274
|
+
g.logbrew_span_captured = True
|
|
275
|
+
if config.capture_request_metrics:
|
|
276
|
+
capture_request_metric(
|
|
277
|
+
config.client,
|
|
278
|
+
status_code=response.status_code,
|
|
279
|
+
duration_ms=duration_ms,
|
|
280
|
+
metric_name=config.request_metric_name,
|
|
281
|
+
)
|
|
282
|
+
if should_capture_request_span or config.capture_request_metrics:
|
|
283
|
+
flush_if_configured(config)
|
|
284
|
+
return response
|
|
285
|
+
|
|
286
|
+
@app.teardown_request
|
|
287
|
+
def logbrew_teardown_request(exc: BaseException | None) -> None:
|
|
288
|
+
if (
|
|
289
|
+
exc is not None
|
|
290
|
+
and not isinstance(exc, (SdkError, TransportError))
|
|
291
|
+
and config.capture_exceptions
|
|
292
|
+
and not bool(getattr(g, "logbrew_exception_captured", False))
|
|
293
|
+
):
|
|
294
|
+
capture_exception_and_span(config, exc)
|
|
295
|
+
activation = getattr(g, "logbrew_trace_activation", None)
|
|
296
|
+
if activation is not None:
|
|
297
|
+
activation.__exit__(None, None, None)
|
|
298
|
+
|
|
299
|
+
def logbrew_got_request_exception(sender: Flask, exception: BaseException, **_: Any) -> None:
|
|
300
|
+
if (
|
|
301
|
+
sender is app
|
|
302
|
+
and not isinstance(exception, (SdkError, TransportError))
|
|
303
|
+
and config.capture_exceptions
|
|
304
|
+
and not bool(getattr(g, "logbrew_exception_captured", False))
|
|
305
|
+
):
|
|
306
|
+
capture_exception_and_span(config, exception)
|
|
307
|
+
|
|
308
|
+
got_request_exception.connect(logbrew_got_request_exception, app, weak=False)
|
|
309
|
+
return config
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def capture_exception_and_span(config: LogBrewFlaskConfig, exc: BaseException) -> None:
|
|
313
|
+
"""Capture an exception issue and matching error span for the active request."""
|
|
314
|
+
|
|
315
|
+
duration_ms = request_duration_ms()
|
|
316
|
+
trace_context = request_logbrew_trace()
|
|
317
|
+
capture_exception(config.client, exc, trace=trace_context)
|
|
318
|
+
capture_request_span(
|
|
319
|
+
config.client,
|
|
320
|
+
status_code=500,
|
|
321
|
+
duration_ms=duration_ms,
|
|
322
|
+
span_id_factory=config.span_id_factory,
|
|
323
|
+
trace=trace_context,
|
|
324
|
+
)
|
|
325
|
+
g.logbrew_exception_captured = True
|
|
326
|
+
g.logbrew_span_captured = True
|
|
327
|
+
if config.capture_request_metrics:
|
|
328
|
+
capture_request_metric(
|
|
329
|
+
config.client,
|
|
330
|
+
status_code=500,
|
|
331
|
+
duration_ms=duration_ms,
|
|
332
|
+
metric_name=config.request_metric_name,
|
|
333
|
+
)
|
|
334
|
+
flush_if_configured(config)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def flush_if_configured(config: LogBrewFlaskConfig) -> None:
|
|
338
|
+
"""Flush the configured client transport if response-path flushing is enabled."""
|
|
339
|
+
|
|
340
|
+
if not config.flush_on_response or config.transport is None:
|
|
341
|
+
return
|
|
342
|
+
try:
|
|
343
|
+
config.client.flush(config.transport)
|
|
344
|
+
except (SdkError, TransportError):
|
|
345
|
+
if config.raise_flush_errors:
|
|
346
|
+
raise
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def request_duration_ms() -> float:
|
|
350
|
+
"""Return elapsed request duration in milliseconds."""
|
|
351
|
+
|
|
352
|
+
started_at = getattr(g, "logbrew_started_at", None)
|
|
353
|
+
if not isinstance(started_at, float):
|
|
354
|
+
return 0.0
|
|
355
|
+
return max(0.0, (time.perf_counter() - started_at) * 1000)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def default_span_id_factory() -> str:
|
|
359
|
+
"""Return a fresh W3C-compatible child span id."""
|
|
360
|
+
|
|
361
|
+
span_id = uuid.uuid4().hex[:16]
|
|
362
|
+
return "0000000000000001" if span_id == "0000000000000000" else span_id
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def create_request_trace_context(
|
|
366
|
+
*,
|
|
367
|
+
span_id_factory: Callable[[], str] | None = None,
|
|
368
|
+
) -> LogBrewTraceContext:
|
|
369
|
+
"""Return a privacy-safe request-local trace context for Flask telemetry."""
|
|
370
|
+
|
|
371
|
+
traceparent = request.headers.get("traceparent")
|
|
372
|
+
if traceparent:
|
|
373
|
+
try:
|
|
374
|
+
parse_traceparent(traceparent)
|
|
375
|
+
return create_logbrew_trace_context(traceparent, span_id_factory=span_id_factory)
|
|
376
|
+
except SdkError:
|
|
377
|
+
pass
|
|
378
|
+
return create_logbrew_trace_context(span_id_factory=span_id_factory)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def request_logbrew_trace() -> LogBrewTraceContext | None:
|
|
382
|
+
"""Return the trace context attached to the current Flask request, when present."""
|
|
383
|
+
|
|
384
|
+
trace = getattr(g, "logbrew_trace", None)
|
|
385
|
+
return trace if isinstance(trace, LogBrewTraceContext) else None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
__all__ = [
|
|
389
|
+
"LogBrewFlaskConfig",
|
|
390
|
+
"add_logbrew_middleware",
|
|
391
|
+
"capture_exception",
|
|
392
|
+
"capture_request_metric",
|
|
393
|
+
"capture_request_span",
|
|
394
|
+
"create_request_metric_attributes",
|
|
395
|
+
"create_request_trace_context",
|
|
396
|
+
"get_active_logbrew_trace",
|
|
397
|
+
"request_logbrew_trace",
|
|
398
|
+
"request_metadata",
|
|
399
|
+
"request_name",
|
|
400
|
+
"request_route_template",
|
|
401
|
+
"utc_timestamp",
|
|
402
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runnable examples shipped with logbrew-flask."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import runpy
|
|
5
|
+
|
|
6
|
+
EXAMPLES = {
|
|
7
|
+
"readme-example": "logbrew_flask.examples.readme_example",
|
|
8
|
+
"real-user-smoke": "logbrew_flask.examples.real_user_smoke",
|
|
9
|
+
"outbound-http": "logbrew_flask.examples.outbound_http",
|
|
10
|
+
"dependency-spans": "logbrew_flask.examples.dependency_spans",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser(description="Run packaged logbrew-flask examples.")
|
|
16
|
+
parser.add_argument("example", nargs="?", default="real-user-smoke", choices=sorted(EXAMPLES))
|
|
17
|
+
parser.add_argument("--list", action="store_true", help="List available examples and exit.")
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
if args.list:
|
|
21
|
+
print("readme-example -> python -m logbrew_flask.examples readme-example")
|
|
22
|
+
print("real-user-smoke -> python -m logbrew_flask.examples real-user-smoke")
|
|
23
|
+
print("outbound-http -> python -m logbrew_flask.examples outbound-http")
|
|
24
|
+
print("dependency-spans -> python -m logbrew_flask.examples dependency-spans")
|
|
25
|
+
print("default (real-user-smoke) -> python -m logbrew_flask.examples")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
runpy.run_module(EXAMPLES[args.example], run_name="__main__")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from flask import Flask
|
|
9
|
+
from logbrew_sdk import (
|
|
10
|
+
LogBrewClient,
|
|
11
|
+
RecordingTransport,
|
|
12
|
+
cache_operation_with_logbrew_span,
|
|
13
|
+
database_operation_with_logbrew_span,
|
|
14
|
+
queue_operation_with_logbrew_span,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from logbrew_flask import add_logbrew_middleware, get_active_logbrew_trace
|
|
18
|
+
|
|
19
|
+
client = LogBrewClient.create(
|
|
20
|
+
api_key="LOGBREW_API_KEY",
|
|
21
|
+
sdk_name="logbrew-flask",
|
|
22
|
+
sdk_version="0.1.0",
|
|
23
|
+
)
|
|
24
|
+
transport = RecordingTransport.always_accept()
|
|
25
|
+
app = Flask(__name__)
|
|
26
|
+
add_logbrew_middleware(app, client=client, transport=transport, span_id_factory=lambda: "b7ad6b7169203331")
|
|
27
|
+
|
|
28
|
+
database = sqlite3.connect(":memory:", check_same_thread=False)
|
|
29
|
+
database.execute("CREATE TABLE inventory (sku TEXT PRIMARY KEY, quantity INTEGER NOT NULL)")
|
|
30
|
+
database.execute("INSERT INTO inventory (sku, quantity) VALUES (?, ?)", ("sku_123", 4))
|
|
31
|
+
database.commit()
|
|
32
|
+
cache: dict[str, int] = {"sku_123": 4}
|
|
33
|
+
queue: list[str] = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def select_inventory() -> sqlite3.Row | tuple[Any, ...] | None:
|
|
37
|
+
return cast(
|
|
38
|
+
sqlite3.Row | tuple[Any, ...] | None,
|
|
39
|
+
database.execute("SELECT quantity FROM inventory WHERE sku = ?", ("sku_123",)).fetchone(),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read_inventory_cache() -> int:
|
|
44
|
+
return cache["sku_123"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def publish_checkout_event() -> int:
|
|
48
|
+
queue.append("checkout.completed")
|
|
49
|
+
return len(queue)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.post("/checkout/<order_id>")
|
|
53
|
+
def checkout(order_id: str) -> dict[str, object]:
|
|
54
|
+
trace = get_active_logbrew_trace()
|
|
55
|
+
inventory_row = database_operation_with_logbrew_span(
|
|
56
|
+
"SELECT inventory",
|
|
57
|
+
client=client,
|
|
58
|
+
event_id="evt_flask_dependency_database",
|
|
59
|
+
operation=select_inventory,
|
|
60
|
+
system="sqlite",
|
|
61
|
+
db_name="checkout",
|
|
62
|
+
statement_template="SELECT inventory WHERE sku = ?",
|
|
63
|
+
row_count=1,
|
|
64
|
+
metadata={"dependency": "inventory"},
|
|
65
|
+
span_id_factory=lambda: "c8ad6b7169203332",
|
|
66
|
+
)
|
|
67
|
+
inventory_count = int(inventory_row[0]) if inventory_row else 0
|
|
68
|
+
cached_count = cache_operation_with_logbrew_span(
|
|
69
|
+
"GET inventory",
|
|
70
|
+
client=client,
|
|
71
|
+
event_id="evt_flask_dependency_cache",
|
|
72
|
+
operation=read_inventory_cache,
|
|
73
|
+
system="memory-cache",
|
|
74
|
+
cache_name="inventory-cache",
|
|
75
|
+
cache_hit=True,
|
|
76
|
+
item_count=1,
|
|
77
|
+
metadata={"dependency": "inventory-cache"},
|
|
78
|
+
span_id_factory=lambda: "d9ad6b7169203333",
|
|
79
|
+
)
|
|
80
|
+
published_count = queue_operation_with_logbrew_span(
|
|
81
|
+
"PUBLISH checkout.completed",
|
|
82
|
+
client=client,
|
|
83
|
+
event_id="evt_flask_dependency_queue",
|
|
84
|
+
operation=publish_checkout_event,
|
|
85
|
+
system="memory-queue",
|
|
86
|
+
operation_kind="publish",
|
|
87
|
+
queue_name="checkout-events",
|
|
88
|
+
task_name="checkout.completed",
|
|
89
|
+
message_count=1,
|
|
90
|
+
metadata={"dependency": "checkout-events"},
|
|
91
|
+
span_id_factory=lambda: "e0ad6b7169203334",
|
|
92
|
+
)
|
|
93
|
+
return {
|
|
94
|
+
"ok": inventory_count == cached_count and published_count == 1,
|
|
95
|
+
"orderId": order_id,
|
|
96
|
+
"traceId": trace.trace_id if trace else None,
|
|
97
|
+
"spanId": trace.span_id if trace else None,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
http = app.test_client()
|
|
102
|
+
checkout_response = http.post(
|
|
103
|
+
"/checkout/order_123?debug=true",
|
|
104
|
+
headers={"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
events: list[dict[str, Any]] = []
|
|
108
|
+
for body in transport.sent_bodies:
|
|
109
|
+
payload = cast(dict[str, Any], json.loads(body))
|
|
110
|
+
events.extend(cast(list[dict[str, Any]], payload["events"]))
|
|
111
|
+
spans = {
|
|
112
|
+
cast(str, event["attributes"]["name"]): cast(dict[str, Any], event["attributes"])
|
|
113
|
+
for event in events
|
|
114
|
+
if event["type"] == "span"
|
|
115
|
+
}
|
|
116
|
+
request_span = spans["POST /checkout/<order_id>"]
|
|
117
|
+
database_span = spans["sqlite SELECT inventory"]
|
|
118
|
+
cache_span = spans["memory-cache GET inventory"]
|
|
119
|
+
queue_span = spans["memory-queue PUBLISH checkout.completed"]
|
|
120
|
+
|
|
121
|
+
print(json.dumps({"sdk": client.sdk, "events": events}, indent=2))
|
|
122
|
+
print(
|
|
123
|
+
json.dumps(
|
|
124
|
+
{
|
|
125
|
+
"checkoutStatus": checkout_response.status_code,
|
|
126
|
+
"events": len(events),
|
|
127
|
+
"requestSpanId": request_span["spanId"],
|
|
128
|
+
"databaseParentSpanId": database_span["parentSpanId"],
|
|
129
|
+
"databaseSpanId": database_span["spanId"],
|
|
130
|
+
"cacheParentSpanId": cache_span["parentSpanId"],
|
|
131
|
+
"cacheSpanId": cache_span["spanId"],
|
|
132
|
+
"queueParentSpanId": queue_span["parentSpanId"],
|
|
133
|
+
"queueSpanId": queue_span["spanId"],
|
|
134
|
+
"dependencySpanNames": [database_span["name"], cache_span["name"], queue_span["name"]],
|
|
135
|
+
},
|
|
136
|
+
indent=2,
|
|
137
|
+
),
|
|
138
|
+
file=sys.stderr,
|
|
139
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from flask import Flask
|
|
9
|
+
from logbrew_sdk import (
|
|
10
|
+
LogBrewClient,
|
|
11
|
+
RecordingTransport,
|
|
12
|
+
requests_request_with_logbrew_span,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from logbrew_flask import add_logbrew_middleware, get_active_logbrew_trace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class FakePaymentResponse:
|
|
20
|
+
status_code: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
client = LogBrewClient.create(
|
|
24
|
+
api_key="LOGBREW_API_KEY",
|
|
25
|
+
sdk_name="logbrew-flask",
|
|
26
|
+
sdk_version="0.1.0",
|
|
27
|
+
)
|
|
28
|
+
transport = RecordingTransport.always_accept()
|
|
29
|
+
app = Flask(__name__)
|
|
30
|
+
add_logbrew_middleware(app, client=client, transport=transport, span_id_factory=lambda: "b7ad6b7169203331")
|
|
31
|
+
captured_outbound_headers: dict[str, str] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fake_payment_request(method: str, url: str, **kwargs: Any) -> FakePaymentResponse:
|
|
35
|
+
"""Caller-owned request seam used by the example instead of making a network call."""
|
|
36
|
+
|
|
37
|
+
headers = kwargs.get("headers")
|
|
38
|
+
if isinstance(headers, dict):
|
|
39
|
+
captured_outbound_headers.update({str(name): str(value) for name, value in headers.items()})
|
|
40
|
+
return FakePaymentResponse(status_code=202)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.post("/checkout/<order_id>")
|
|
44
|
+
def checkout(order_id: str) -> dict[str, object]:
|
|
45
|
+
trace = get_active_logbrew_trace()
|
|
46
|
+
response = requests_request_with_logbrew_span(
|
|
47
|
+
"POST",
|
|
48
|
+
"https://payments.example.test/payments/authorize?debug_marker=drop",
|
|
49
|
+
client=client,
|
|
50
|
+
event_id="evt_flask_outbound_payment",
|
|
51
|
+
request=fake_payment_request,
|
|
52
|
+
route_template="/payments/authorize",
|
|
53
|
+
metadata={"dependency": "payments", "operation": "authorize"},
|
|
54
|
+
span_id_factory=lambda: "c8ad6b7169203332",
|
|
55
|
+
)
|
|
56
|
+
return {
|
|
57
|
+
"ok": True,
|
|
58
|
+
"orderId": order_id,
|
|
59
|
+
"traceId": trace.trace_id if trace else None,
|
|
60
|
+
"spanId": trace.span_id if trace else None,
|
|
61
|
+
"paymentStatus": response.status_code,
|
|
62
|
+
"outboundTraceparent": captured_outbound_headers.get("traceparent"),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
http = app.test_client()
|
|
67
|
+
checkout_response = http.post(
|
|
68
|
+
"/checkout/order_123?debug=true",
|
|
69
|
+
headers={"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
events: list[dict[str, Any]] = []
|
|
73
|
+
for body in transport.sent_bodies:
|
|
74
|
+
payload = cast(dict[str, Any], json.loads(body))
|
|
75
|
+
events.extend(cast(list[dict[str, Any]], payload["events"]))
|
|
76
|
+
checkout_payload = cast(dict[str, Any], checkout_response.get_json())
|
|
77
|
+
spans = {
|
|
78
|
+
cast(str, event["attributes"]["name"]): cast(dict[str, Any], event["attributes"])
|
|
79
|
+
for event in events
|
|
80
|
+
if event["type"] == "span"
|
|
81
|
+
}
|
|
82
|
+
request_span = spans["POST /checkout/<order_id>"]
|
|
83
|
+
outbound_span = spans["POST /payments/authorize"]
|
|
84
|
+
outbound_metadata = cast(dict[str, Any], outbound_span["metadata"])
|
|
85
|
+
|
|
86
|
+
print(json.dumps({"sdk": client.sdk, "events": events}, indent=2))
|
|
87
|
+
print(
|
|
88
|
+
json.dumps(
|
|
89
|
+
{
|
|
90
|
+
"checkoutStatus": checkout_response.status_code,
|
|
91
|
+
"events": len(events),
|
|
92
|
+
"requestSpanId": request_span["spanId"],
|
|
93
|
+
"outboundParentSpanId": outbound_span["parentSpanId"],
|
|
94
|
+
"outboundSpanId": outbound_span["spanId"],
|
|
95
|
+
"outboundTraceparent": checkout_payload["outboundTraceparent"],
|
|
96
|
+
"outboundRoute": outbound_metadata["routeTemplate"],
|
|
97
|
+
"outboundSource": outbound_metadata["source"],
|
|
98
|
+
},
|
|
99
|
+
indent=2,
|
|
100
|
+
),
|
|
101
|
+
file=sys.stderr,
|
|
102
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from flask import Flask
|
|
7
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport
|
|
8
|
+
|
|
9
|
+
from logbrew_flask import add_logbrew_middleware
|
|
10
|
+
|
|
11
|
+
client = LogBrewClient.create(
|
|
12
|
+
api_key="LOGBREW_API_KEY",
|
|
13
|
+
sdk_name="logbrew-flask",
|
|
14
|
+
sdk_version="0.1.0",
|
|
15
|
+
)
|
|
16
|
+
transport = RecordingTransport.always_accept()
|
|
17
|
+
app = Flask(__name__)
|
|
18
|
+
add_logbrew_middleware(app, client=client, transport=transport)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.get("/health")
|
|
22
|
+
def health() -> dict[str, bool]:
|
|
23
|
+
return {"ok": True}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
response = app.test_client().get("/health")
|
|
27
|
+
print(json.dumps({"status": response.status_code, "events": client.pending_events()}), file=sys.stderr)
|
|
28
|
+
print(transport.sent_bodies[-1])
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from flask import Flask
|
|
9
|
+
from logbrew_sdk import LogBrewClient, LogBrewLoggingHandler, RecordingTransport
|
|
10
|
+
|
|
11
|
+
from logbrew_flask import add_logbrew_middleware, get_active_logbrew_trace
|
|
12
|
+
|
|
13
|
+
client = LogBrewClient.create(
|
|
14
|
+
api_key="LOGBREW_API_KEY",
|
|
15
|
+
sdk_name="logbrew-flask",
|
|
16
|
+
sdk_version="0.1.0",
|
|
17
|
+
)
|
|
18
|
+
transport = RecordingTransport.always_accept()
|
|
19
|
+
logger = logging.getLogger("flask.checkout.example")
|
|
20
|
+
logger.handlers = []
|
|
21
|
+
logger.propagate = False
|
|
22
|
+
logger.setLevel(logging.INFO)
|
|
23
|
+
logger.addHandler(LogBrewLoggingHandler(client, metadata={"service": "checkout"}))
|
|
24
|
+
app = Flask(__name__)
|
|
25
|
+
add_logbrew_middleware(app, client=client, transport=transport, span_id_factory=lambda: "b7ad6b7169203331")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.get("/health")
|
|
29
|
+
def health() -> dict[str, object]:
|
|
30
|
+
trace = get_active_logbrew_trace()
|
|
31
|
+
logger.info("health request", extra={"route_template": "/health"})
|
|
32
|
+
return {
|
|
33
|
+
"ok": True,
|
|
34
|
+
"traceId": trace.trace_id if trace else None,
|
|
35
|
+
"spanId": trace.span_id if trace else None,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.get("/boom")
|
|
40
|
+
def boom() -> dict[str, bool]:
|
|
41
|
+
raise RuntimeError("broken handler")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
http = app.test_client()
|
|
45
|
+
health_response = http.get(
|
|
46
|
+
"/health?debug=true",
|
|
47
|
+
headers={"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"},
|
|
48
|
+
)
|
|
49
|
+
boom_response = http.get("/boom")
|
|
50
|
+
|
|
51
|
+
events: list[dict[str, Any]] = []
|
|
52
|
+
for body in transport.sent_bodies:
|
|
53
|
+
payload = cast(dict[str, Any], json.loads(body))
|
|
54
|
+
events.extend(cast(list[dict[str, Any]], payload["events"]))
|
|
55
|
+
health_payload = cast(dict[str, Any], health_response.get_json())
|
|
56
|
+
first_span_attributes = cast(dict[str, Any], events[1]["attributes"])
|
|
57
|
+
first_span_metadata = cast(dict[str, Any], first_span_attributes["metadata"])
|
|
58
|
+
|
|
59
|
+
print(json.dumps({"sdk": client.sdk, "events": events}, indent=2))
|
|
60
|
+
print(
|
|
61
|
+
json.dumps(
|
|
62
|
+
{
|
|
63
|
+
"healthStatus": health_response.status_code,
|
|
64
|
+
"boomStatus": boom_response.status_code,
|
|
65
|
+
"events": len(events),
|
|
66
|
+
"traceId": health_payload["traceId"],
|
|
67
|
+
"spanId": health_payload["spanId"],
|
|
68
|
+
"parentSpanId": first_span_attributes["parentSpanId"],
|
|
69
|
+
"path": first_span_metadata["path"],
|
|
70
|
+
},
|
|
71
|
+
indent=2,
|
|
72
|
+
),
|
|
73
|
+
file=sys.stderr,
|
|
74
|
+
)
|
logbrew_flask/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logbrew-flask
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Flask integration for capturing LogBrew request spans and exceptions.
|
|
5
|
+
Author: LogBrew
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/LogBrewCo/sdk
|
|
8
|
+
Keywords: logbrew,observability,flask,wsgi,logs
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Flask
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: Flask>=3.1
|
|
21
|
+
Requires-Dist: logbrew-sdk<0.2.0,>=0.1.1
|
|
22
|
+
|
|
23
|
+
# logbrew-flask
|
|
24
|
+
|
|
25
|
+
Flask integration for capturing LogBrew request spans and exceptions with the public Python SDK.
|
|
26
|
+
|
|
27
|
+
This package is source-only until its first PyPI release. The pip command below requires the package to be available on PyPI; use a local checkout or local wheel when evaluating it before release.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m pip install logbrew-sdk logbrew-flask
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package is typed, ships `py.typed`, depends on the core `logbrew-sdk`, and keeps Flask as a normal framework dependency instead of monkeypatching Flask globally.
|
|
34
|
+
|
|
35
|
+
Use a project-scoped server ingest key, for example `LOGBREW_SERVER_API_KEY`.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
from flask import Flask
|
|
41
|
+
from logbrew_flask import add_logbrew_middleware
|
|
42
|
+
from logbrew_sdk import LogBrewClient
|
|
43
|
+
|
|
44
|
+
client = LogBrewClient.create(
|
|
45
|
+
api_key=os.environ["LOGBREW_SERVER_API_KEY"],
|
|
46
|
+
release=os.environ.get("LOGBREW_RELEASE"),
|
|
47
|
+
environment=os.environ.get("LOGBREW_ENVIRONMENT"),
|
|
48
|
+
sdk_name="checkout-api",
|
|
49
|
+
sdk_version="1.0.0",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
app = Flask(__name__)
|
|
53
|
+
add_logbrew_middleware(app, client=client)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.get("/health")
|
|
57
|
+
def health() -> dict[str, bool]:
|
|
58
|
+
return {"ok": True}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What It Captures
|
|
62
|
+
|
|
63
|
+
The middleware records one request span for each captured response. It can also record request duration metrics and exception issues.
|
|
64
|
+
|
|
65
|
+
Request spans use the Flask route template, such as `GET /orders/<int:order_id>`, for low-noise grouping. Span metadata includes `routeTemplate`; concrete dynamic paths are not emitted when a route template is available. Valid inbound W3C `traceparent` headers are continued with a fresh child span id.
|
|
66
|
+
|
|
67
|
+
Handlers can call `get_active_logbrew_trace()` or use `LogBrewLoggingHandler`; logs emitted during the request share the active request trace and span.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from logbrew_flask import get_active_logbrew_trace
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.get("/orders/<int:order_id>")
|
|
74
|
+
def order_detail(order_id: int) -> dict[str, str | None]:
|
|
75
|
+
trace = get_active_logbrew_trace()
|
|
76
|
+
return {"traceId": trace.trace_id if trace else None}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Set `capture_request_metrics=True` to emit an explicit `http.server.duration` histogram for each request. Apps can pass `span_id_factory` when deterministic child span ids are useful for controlled diagnostics; production apps usually let LogBrew generate span ids.
|
|
80
|
+
|
|
81
|
+
## Outbound HTTP Child Spans
|
|
82
|
+
|
|
83
|
+
When a handler calls another service, use the core Python HTTP helpers inside the Flask request. They automatically reuse the active Flask request trace, create a child span, and inject one W3C `traceparent` header whose span id matches the emitted outbound span.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from logbrew_sdk import requests_request_with_logbrew_span
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.post("/checkout/<order_id>")
|
|
90
|
+
def checkout(order_id: str) -> dict[str, bool]:
|
|
91
|
+
response = requests_request_with_logbrew_span(
|
|
92
|
+
"POST",
|
|
93
|
+
"https://payments.example.com/payments/authorize",
|
|
94
|
+
client=client,
|
|
95
|
+
event_id="evt_checkout_payment",
|
|
96
|
+
route_template="/payments/authorize",
|
|
97
|
+
)
|
|
98
|
+
return {"accepted": response.status_code == 202}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The helper does not patch `requests` globally. It records method, low-cardinality route template, status code, trace id, span id, and parent span id. It does not capture full URLs, query strings, request bodies, response bodies, arbitrary headers, cookies, baggage, or tracestate.
|
|
102
|
+
|
|
103
|
+
## Database, Cache, And Queue Child Spans
|
|
104
|
+
|
|
105
|
+
Use the core dependency helpers inside a Flask handler to connect database, cache, and queue work to the active request trace.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from logbrew_sdk import (
|
|
109
|
+
cache_operation_with_logbrew_span,
|
|
110
|
+
database_operation_with_logbrew_span,
|
|
111
|
+
queue_operation_with_logbrew_span,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.post("/checkout/<order_id>")
|
|
116
|
+
def checkout(order_id: str) -> dict[str, bool]:
|
|
117
|
+
inventory = database_operation_with_logbrew_span(
|
|
118
|
+
"SELECT inventory",
|
|
119
|
+
client=client,
|
|
120
|
+
operation=lambda: database.execute("SELECT quantity FROM inventory WHERE sku = ?", ("sku_123",)).fetchone(),
|
|
121
|
+
system="sqlite",
|
|
122
|
+
statement_template="SELECT inventory WHERE sku = ?",
|
|
123
|
+
)
|
|
124
|
+
cached_inventory = cache_operation_with_logbrew_span(
|
|
125
|
+
"GET inventory",
|
|
126
|
+
client=client,
|
|
127
|
+
operation=lambda: cache["sku_123"],
|
|
128
|
+
system="memory-cache",
|
|
129
|
+
cache_name="inventory-cache",
|
|
130
|
+
cache_hit=True,
|
|
131
|
+
)
|
|
132
|
+
published = queue_operation_with_logbrew_span(
|
|
133
|
+
"PUBLISH checkout.completed",
|
|
134
|
+
client=client,
|
|
135
|
+
operation=lambda: queue.append("checkout.completed") or len(queue),
|
|
136
|
+
system="memory-queue",
|
|
137
|
+
operation_kind="publish",
|
|
138
|
+
queue_name="checkout-events",
|
|
139
|
+
task_name="checkout.completed",
|
|
140
|
+
)
|
|
141
|
+
return {"ok": inventory is not None and cached_inventory > 0 and published == 1}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Run `python -m logbrew_flask.examples dependency-spans` to see a request span with database, cache, and queue child spans under the same trace. These helpers record operation names, systems, status, trace ids, span ids, parent span ids, and primitive metadata. They do not capture SQL bind values, result payloads, queue message payloads, cache values, arbitrary headers, baggage, or tracestate.
|
|
145
|
+
|
|
146
|
+
## Privacy Defaults
|
|
147
|
+
|
|
148
|
+
LogBrew does not capture request bodies, response bodies, cookies, arbitrary headers, query strings, raw `traceparent` values, baggage, or tracestate. Exception issues include the exception type and message but not stack frames.
|
|
149
|
+
|
|
150
|
+
## Delivery Failures
|
|
151
|
+
|
|
152
|
+
By default, transport failures do not break the Flask response path. Set `raise_flush_errors=True` only when your app wants delivery failures to surface as request errors in controlled diagnostics.
|
|
153
|
+
|
|
154
|
+
## Tradeoff
|
|
155
|
+
|
|
156
|
+
Sentry, Datadog, and OpenTelemetry provide broader automatic Flask, outbound HTTP, and dependency instrumentation, including global patching and deeper view/template/client hooks. LogBrew starts with explicit app-owned Flask and dependency helpers because that keeps setup reversible, simple to reason about, and safer for privacy-sensitive services.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
logbrew_flask/__init__.py,sha256=TcVP7cKbXi_USN_t4aWA_SBSEIg40FRxEnBfUpj8WKo,13182
|
|
2
|
+
logbrew_flask/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
3
|
+
logbrew_flask/examples/__init__.py,sha256=HIu2aAxwQQuMjNX1EMHiwgfLl1DeLSm1bqHhm_wfVXA,52
|
|
4
|
+
logbrew_flask/examples/__main__.py,sha256=cJ6Heq5DXyvp7z5WbDdiaHuj2a3tNht2UugKEVx6WaE,1231
|
|
5
|
+
logbrew_flask/examples/dependency_spans.py,sha256=dWyxWzyFeWPdQRrHg_Yg9QnTb9PP3FmQmELFj8ekSnU,4656
|
|
6
|
+
logbrew_flask/examples/outbound_http.py,sha256=jw6lqMwuyEn6tQwy_9hD9adW3enDRjt7obs6JNpjDpQ,3398
|
|
7
|
+
logbrew_flask/examples/readme_example.py,sha256=OKww66742SH0hFjg8EZDnmPryWqxK12p9HBDLszCcLs,707
|
|
8
|
+
logbrew_flask/examples/real_user_smoke.py,sha256=dGIS6ktQ9iI8473OdJFewEOLgjq45G4bVuxeKApF6UU,2324
|
|
9
|
+
logbrew_flask-0.1.1.dist-info/METADATA,sha256=TfQ-23GE4S74UkVN3Adl4TSXDhnalQxcTjQxhmwmtNk,6765
|
|
10
|
+
logbrew_flask-0.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
logbrew_flask-0.1.1.dist-info/top_level.txt,sha256=Ipu3qZol4tpp1xzIgP4O3J1kJ6JyYIUj3ymHfvOgv8o,14
|
|
12
|
+
logbrew_flask-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
logbrew_flask
|