logbrew-django 0.1.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.
- logbrew_django-0.1.0/PKG-INFO +74 -0
- logbrew_django-0.1.0/README.md +52 -0
- logbrew_django-0.1.0/pyproject.toml +42 -0
- logbrew_django-0.1.0/setup.cfg +4 -0
- logbrew_django-0.1.0/src/logbrew_django/__init__.py +298 -0
- logbrew_django-0.1.0/src/logbrew_django/examples/__init__.py +1 -0
- logbrew_django-0.1.0/src/logbrew_django/examples/__main__.py +29 -0
- logbrew_django-0.1.0/src/logbrew_django/examples/readme_example.py +49 -0
- logbrew_django-0.1.0/src/logbrew_django/examples/real_user_smoke.py +80 -0
- logbrew_django-0.1.0/src/logbrew_django/py.typed +1 -0
- logbrew_django-0.1.0/src/logbrew_django.egg-info/PKG-INFO +74 -0
- logbrew_django-0.1.0/src/logbrew_django.egg-info/SOURCES.txt +14 -0
- logbrew_django-0.1.0/src/logbrew_django.egg-info/dependency_links.txt +1 -0
- logbrew_django-0.1.0/src/logbrew_django.egg-info/requires.txt +2 -0
- logbrew_django-0.1.0/src/logbrew_django.egg-info/top_level.txt +1 -0
- logbrew_django-0.1.0/tests/test_django_integration.py +165 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logbrew-django
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django 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,django,middleware,logs
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Django
|
|
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: Django>=5.2
|
|
21
|
+
Requires-Dist: logbrew-sdk==0.1.0
|
|
22
|
+
|
|
23
|
+
# logbrew-django
|
|
24
|
+
|
|
25
|
+
Django integration for capturing LogBrew request spans and exceptions with the public Python SDK.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m pip install logbrew-sdk logbrew-django
|
|
31
|
+
python3 -m logbrew_django.examples --help
|
|
32
|
+
python3 -m logbrew_django.examples --list
|
|
33
|
+
python3 -m logbrew_django.examples readme-example
|
|
34
|
+
python3 -m logbrew_django.examples real-user-smoke
|
|
35
|
+
python3 -m logbrew_django.examples
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The package is typed, ships `py.typed`, depends on the core `logbrew-sdk`, and keeps Django as a normal framework dependency instead of owning the user's project layout.
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# settings.py
|
|
44
|
+
MIDDLEWARE = [
|
|
45
|
+
"logbrew_django.LogBrewDjangoMiddleware",
|
|
46
|
+
*MIDDLEWARE,
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# app startup code
|
|
52
|
+
from logbrew_django import configure_logbrew
|
|
53
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport
|
|
54
|
+
|
|
55
|
+
client = LogBrewClient.create(
|
|
56
|
+
api_key="LOGBREW_API_KEY",
|
|
57
|
+
sdk_name="logbrew-django",
|
|
58
|
+
sdk_version="0.1.0",
|
|
59
|
+
)
|
|
60
|
+
transport = RecordingTransport.always_accept()
|
|
61
|
+
configure_logbrew(
|
|
62
|
+
client=client,
|
|
63
|
+
transport=transport,
|
|
64
|
+
span_id_factory=lambda: "b7ad6b7169203331",
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`LogBrewDjangoMiddleware` records successful requests as span events, records unhandled view exceptions as issue plus error-span events, and flushes through the configured transport after each response. If no transport is provided, events stay queued on the core client so the project can flush them itself.
|
|
69
|
+
|
|
70
|
+
When an incoming request has a valid W3C `traceparent` header, request capture continues that trace by using the incoming `traceId` and parent span id while creating a fresh child span id. Missing or malformed `traceparent` headers keep the existing synthetic request span behavior so bad client headers do not break the project. Automatic metadata uses the request path without query text. Use `span_id_factory` only when tests need deterministic child span ids.
|
|
71
|
+
|
|
72
|
+
By default, transport failures do not break the Django response path. Set `raise_flush_errors=True` in test environments when you want misconfigured transport behavior to fail loudly.
|
|
73
|
+
|
|
74
|
+
Use a clearly fake placeholder like `LOGBREW_API_KEY` in local examples and tests.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# logbrew-django
|
|
2
|
+
|
|
3
|
+
Django integration for capturing LogBrew request spans and exceptions with the public Python SDK.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 -m pip install logbrew-sdk logbrew-django
|
|
9
|
+
python3 -m logbrew_django.examples --help
|
|
10
|
+
python3 -m logbrew_django.examples --list
|
|
11
|
+
python3 -m logbrew_django.examples readme-example
|
|
12
|
+
python3 -m logbrew_django.examples real-user-smoke
|
|
13
|
+
python3 -m logbrew_django.examples
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The package is typed, ships `py.typed`, depends on the core `logbrew-sdk`, and keeps Django as a normal framework dependency instead of owning the user's project layout.
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# settings.py
|
|
22
|
+
MIDDLEWARE = [
|
|
23
|
+
"logbrew_django.LogBrewDjangoMiddleware",
|
|
24
|
+
*MIDDLEWARE,
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
# app startup code
|
|
30
|
+
from logbrew_django import configure_logbrew
|
|
31
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport
|
|
32
|
+
|
|
33
|
+
client = LogBrewClient.create(
|
|
34
|
+
api_key="LOGBREW_API_KEY",
|
|
35
|
+
sdk_name="logbrew-django",
|
|
36
|
+
sdk_version="0.1.0",
|
|
37
|
+
)
|
|
38
|
+
transport = RecordingTransport.always_accept()
|
|
39
|
+
configure_logbrew(
|
|
40
|
+
client=client,
|
|
41
|
+
transport=transport,
|
|
42
|
+
span_id_factory=lambda: "b7ad6b7169203331",
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`LogBrewDjangoMiddleware` records successful requests as span events, records unhandled view exceptions as issue plus error-span events, and flushes through the configured transport after each response. If no transport is provided, events stay queued on the core client so the project can flush them itself.
|
|
47
|
+
|
|
48
|
+
When an incoming request has a valid W3C `traceparent` header, request capture continues that trace by using the incoming `traceId` and parent span id while creating a fresh child span id. Missing or malformed `traceparent` headers keep the existing synthetic request span behavior so bad client headers do not break the project. Automatic metadata uses the request path without query text. Use `span_id_factory` only when tests need deterministic child span ids.
|
|
49
|
+
|
|
50
|
+
By default, transport failures do not break the Django response path. Set `raise_flush_errors=True` in test environments when you want misconfigured transport behavior to fail loudly.
|
|
51
|
+
|
|
52
|
+
Use a clearly fake placeholder like `LOGBREW_API_KEY` in local examples and tests.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "logbrew-django"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Django integration for capturing LogBrew request spans and exceptions."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "LogBrew" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["logbrew", "observability", "django", "middleware", "logs"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Framework :: Django",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Typing :: Typed"
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"Django>=5.2",
|
|
29
|
+
"logbrew-sdk==0.1.0"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/LogBrewCo/sdk"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools]
|
|
36
|
+
package-dir = {"" = "src"}
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
logbrew_django = ["py.typed"]
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Django 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 django.conf import settings
|
|
13
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
14
|
+
from django.http import HttpRequest, HttpResponse
|
|
15
|
+
from logbrew_sdk import (
|
|
16
|
+
LogBrewClient,
|
|
17
|
+
RecordingTransport,
|
|
18
|
+
SdkError,
|
|
19
|
+
SpanAttributes,
|
|
20
|
+
TransportError,
|
|
21
|
+
parse_traceparent,
|
|
22
|
+
span_attributes_from_traceparent,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class LogBrewDjangoConfig:
|
|
28
|
+
"""Runtime options used by the LogBrew Django middleware."""
|
|
29
|
+
|
|
30
|
+
client: LogBrewClient
|
|
31
|
+
transport: RecordingTransport | None = None
|
|
32
|
+
capture_successful_requests: bool = True
|
|
33
|
+
capture_exceptions: bool = True
|
|
34
|
+
flush_on_response: bool = True
|
|
35
|
+
raise_flush_errors: bool = False
|
|
36
|
+
service_name: str = "django"
|
|
37
|
+
span_id_factory: Callable[[], str] | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_configured_state: dict[str, LogBrewDjangoConfig] = {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def configure_logbrew(
|
|
44
|
+
*,
|
|
45
|
+
client: LogBrewClient,
|
|
46
|
+
transport: RecordingTransport | None = None,
|
|
47
|
+
capture_successful_requests: bool = True,
|
|
48
|
+
capture_exceptions: bool = True,
|
|
49
|
+
flush_on_response: bool = True,
|
|
50
|
+
raise_flush_errors: bool = False,
|
|
51
|
+
service_name: str = "django",
|
|
52
|
+
span_id_factory: Callable[[], str] | None = None,
|
|
53
|
+
) -> LogBrewDjangoConfig:
|
|
54
|
+
"""Configure LogBrew Django middleware from application startup code."""
|
|
55
|
+
|
|
56
|
+
config = LogBrewDjangoConfig(
|
|
57
|
+
client=client,
|
|
58
|
+
transport=transport,
|
|
59
|
+
capture_successful_requests=capture_successful_requests,
|
|
60
|
+
capture_exceptions=capture_exceptions,
|
|
61
|
+
flush_on_response=flush_on_response,
|
|
62
|
+
raise_flush_errors=raise_flush_errors,
|
|
63
|
+
service_name=service_name,
|
|
64
|
+
span_id_factory=span_id_factory,
|
|
65
|
+
)
|
|
66
|
+
_configured_state["config"] = config
|
|
67
|
+
return config
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_logbrew_config() -> LogBrewDjangoConfig:
|
|
71
|
+
"""Return the active LogBrew Django config from explicit setup or Django settings."""
|
|
72
|
+
|
|
73
|
+
config = _configured_state.get("config")
|
|
74
|
+
if config is not None:
|
|
75
|
+
return config
|
|
76
|
+
|
|
77
|
+
client = getattr(settings, "LOGBREW_CLIENT", None)
|
|
78
|
+
if not isinstance(client, LogBrewClient):
|
|
79
|
+
raise ImproperlyConfigured(
|
|
80
|
+
"LogBrewDjangoMiddleware requires configure_logbrew(client=...) "
|
|
81
|
+
"or a LOGBREW_CLIENT setting."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
transport = getattr(settings, "LOGBREW_TRANSPORT", None)
|
|
85
|
+
if transport is not None and not isinstance(transport, RecordingTransport):
|
|
86
|
+
raise ImproperlyConfigured("LOGBREW_TRANSPORT must be a RecordingTransport-compatible instance.")
|
|
87
|
+
span_id_factory = getattr(settings, "LOGBREW_SPAN_ID_FACTORY", None)
|
|
88
|
+
if span_id_factory is not None and not callable(span_id_factory):
|
|
89
|
+
raise ImproperlyConfigured("LOGBREW_SPAN_ID_FACTORY must be callable when provided.")
|
|
90
|
+
|
|
91
|
+
return LogBrewDjangoConfig(
|
|
92
|
+
client=client,
|
|
93
|
+
transport=transport,
|
|
94
|
+
capture_successful_requests=bool(getattr(settings, "LOGBREW_CAPTURE_SUCCESSFUL_REQUESTS", True)),
|
|
95
|
+
capture_exceptions=bool(getattr(settings, "LOGBREW_CAPTURE_EXCEPTIONS", True)),
|
|
96
|
+
flush_on_response=bool(getattr(settings, "LOGBREW_FLUSH_ON_RESPONSE", True)),
|
|
97
|
+
raise_flush_errors=bool(getattr(settings, "LOGBREW_RAISE_FLUSH_ERRORS", False)),
|
|
98
|
+
service_name=str(getattr(settings, "LOGBREW_SERVICE_NAME", "django")),
|
|
99
|
+
span_id_factory=span_id_factory,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def utc_timestamp() -> str:
|
|
104
|
+
"""Return a LogBrew-compatible UTC timestamp."""
|
|
105
|
+
|
|
106
|
+
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def request_name(request: HttpRequest) -> str:
|
|
110
|
+
"""Return the stable request name used for span and issue titles."""
|
|
111
|
+
|
|
112
|
+
return f"{request.method} {request.path}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def request_metadata(
|
|
116
|
+
request: HttpRequest,
|
|
117
|
+
*,
|
|
118
|
+
status_code: int | None = None,
|
|
119
|
+
duration_ms: float | None = None,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
"""Return request metadata without including query strings or request bodies."""
|
|
122
|
+
|
|
123
|
+
metadata: dict[str, Any] = {
|
|
124
|
+
"framework": "django",
|
|
125
|
+
"method": request.method,
|
|
126
|
+
"path": request.path,
|
|
127
|
+
}
|
|
128
|
+
resolver_match = getattr(request, "resolver_match", None)
|
|
129
|
+
route = getattr(resolver_match, "route", None)
|
|
130
|
+
view_name = getattr(resolver_match, "view_name", None)
|
|
131
|
+
if isinstance(route, str):
|
|
132
|
+
metadata["route"] = route
|
|
133
|
+
if isinstance(view_name, str):
|
|
134
|
+
metadata["view_name"] = view_name
|
|
135
|
+
if status_code is not None:
|
|
136
|
+
metadata["status_code"] = status_code
|
|
137
|
+
if duration_ms is not None:
|
|
138
|
+
metadata["duration_ms"] = round(duration_ms, 3)
|
|
139
|
+
return metadata
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def capture_request_span(
|
|
143
|
+
client: LogBrewClient,
|
|
144
|
+
request: HttpRequest,
|
|
145
|
+
*,
|
|
146
|
+
status_code: int,
|
|
147
|
+
duration_ms: float,
|
|
148
|
+
event_id: str | None = None,
|
|
149
|
+
timestamp: str | None = None,
|
|
150
|
+
span_id_factory: Callable[[], str] | None = None,
|
|
151
|
+
) -> str:
|
|
152
|
+
"""Capture a Django request as a LogBrew span event and return its event id."""
|
|
153
|
+
|
|
154
|
+
span_event_id = event_id or f"evt_django_span_{uuid.uuid4().hex}"
|
|
155
|
+
span_seed = span_event_id.replace("-", "_")
|
|
156
|
+
traceparent = traceparent_from_request(request)
|
|
157
|
+
attributes: SpanAttributes = {
|
|
158
|
+
"name": request_name(request),
|
|
159
|
+
"traceId": f"trace_{span_seed}",
|
|
160
|
+
"spanId": f"span_{span_seed}",
|
|
161
|
+
"status": "ok" if status_code < 500 else "error",
|
|
162
|
+
"durationMs": duration_ms,
|
|
163
|
+
"metadata": request_metadata(request, status_code=status_code, duration_ms=duration_ms),
|
|
164
|
+
}
|
|
165
|
+
if traceparent:
|
|
166
|
+
try:
|
|
167
|
+
parse_traceparent(traceparent)
|
|
168
|
+
attributes = span_attributes_from_traceparent(
|
|
169
|
+
traceparent,
|
|
170
|
+
name=request_name(request),
|
|
171
|
+
span_id=(span_id_factory or default_span_id_factory)(),
|
|
172
|
+
status="ok" if status_code < 500 else "error",
|
|
173
|
+
duration_ms=duration_ms,
|
|
174
|
+
metadata=request_metadata(request, status_code=status_code, duration_ms=duration_ms),
|
|
175
|
+
)
|
|
176
|
+
except SdkError:
|
|
177
|
+
pass
|
|
178
|
+
client.span(
|
|
179
|
+
span_event_id,
|
|
180
|
+
timestamp or utc_timestamp(),
|
|
181
|
+
attributes,
|
|
182
|
+
)
|
|
183
|
+
return span_event_id
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def capture_exception(
|
|
187
|
+
client: LogBrewClient,
|
|
188
|
+
request: HttpRequest,
|
|
189
|
+
exc: BaseException,
|
|
190
|
+
*,
|
|
191
|
+
event_id: str | None = None,
|
|
192
|
+
timestamp: str | None = None,
|
|
193
|
+
) -> str:
|
|
194
|
+
"""Capture an exception raised while handling a Django request and return its event id."""
|
|
195
|
+
|
|
196
|
+
issue_event_id = event_id or f"evt_django_issue_{uuid.uuid4().hex}"
|
|
197
|
+
client.issue(
|
|
198
|
+
issue_event_id,
|
|
199
|
+
timestamp or utc_timestamp(),
|
|
200
|
+
{
|
|
201
|
+
"title": f"{request_name(request)} failed",
|
|
202
|
+
"level": "error",
|
|
203
|
+
"message": str(exc) or exc.__class__.__name__,
|
|
204
|
+
"metadata": {
|
|
205
|
+
**request_metadata(request, status_code=500),
|
|
206
|
+
"exception_type": exc.__class__.__name__,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
return issue_event_id
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class LogBrewDjangoMiddleware:
|
|
214
|
+
"""Django middleware that records request spans and exception issues with LogBrew."""
|
|
215
|
+
|
|
216
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
217
|
+
self.get_response = get_response
|
|
218
|
+
|
|
219
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
220
|
+
config = get_logbrew_config()
|
|
221
|
+
start = time.perf_counter()
|
|
222
|
+
try:
|
|
223
|
+
response = self.get_response(request)
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
226
|
+
if config.capture_exceptions and request.META.get("logbrew.exception_captured") is not True:
|
|
227
|
+
capture_exception(config.client, request, exc)
|
|
228
|
+
capture_request_span(
|
|
229
|
+
config.client,
|
|
230
|
+
request,
|
|
231
|
+
status_code=500,
|
|
232
|
+
duration_ms=duration_ms,
|
|
233
|
+
span_id_factory=config.span_id_factory,
|
|
234
|
+
)
|
|
235
|
+
self._flush_if_configured(config)
|
|
236
|
+
raise
|
|
237
|
+
|
|
238
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
239
|
+
if config.capture_successful_requests or response.status_code >= 500:
|
|
240
|
+
capture_request_span(
|
|
241
|
+
config.client,
|
|
242
|
+
request,
|
|
243
|
+
status_code=response.status_code,
|
|
244
|
+
duration_ms=duration_ms,
|
|
245
|
+
span_id_factory=config.span_id_factory,
|
|
246
|
+
)
|
|
247
|
+
self._flush_if_configured(config)
|
|
248
|
+
return response
|
|
249
|
+
|
|
250
|
+
def process_exception(self, request: HttpRequest, exception: Exception) -> None:
|
|
251
|
+
"""Capture Django view exceptions before Django converts them into responses."""
|
|
252
|
+
|
|
253
|
+
config = get_logbrew_config()
|
|
254
|
+
if not config.capture_exceptions:
|
|
255
|
+
return None
|
|
256
|
+
capture_exception(config.client, request, exception)
|
|
257
|
+
request.META["logbrew.exception_captured"] = True
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def _flush_if_configured(config: LogBrewDjangoConfig) -> None:
|
|
262
|
+
if not config.flush_on_response or config.transport is None:
|
|
263
|
+
return
|
|
264
|
+
try:
|
|
265
|
+
config.client.flush(config.transport)
|
|
266
|
+
except (SdkError, TransportError):
|
|
267
|
+
if config.raise_flush_errors:
|
|
268
|
+
raise
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def traceparent_from_request(request: HttpRequest) -> str | None:
|
|
272
|
+
"""Return the incoming W3C traceparent header from a Django request."""
|
|
273
|
+
|
|
274
|
+
value = request.headers.get("traceparent")
|
|
275
|
+
if isinstance(value, str):
|
|
276
|
+
return value
|
|
277
|
+
value = request.META.get("HTTP_TRACEPARENT")
|
|
278
|
+
return value if isinstance(value, str) else None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def default_span_id_factory() -> str:
|
|
282
|
+
"""Return a fresh W3C-compatible child span id."""
|
|
283
|
+
|
|
284
|
+
span_id = uuid.uuid4().hex[:16]
|
|
285
|
+
return "0000000000000001" if span_id == "0000000000000000" else span_id
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
__all__ = [
|
|
289
|
+
"LogBrewDjangoConfig",
|
|
290
|
+
"LogBrewDjangoMiddleware",
|
|
291
|
+
"capture_exception",
|
|
292
|
+
"capture_request_span",
|
|
293
|
+
"configure_logbrew",
|
|
294
|
+
"get_logbrew_config",
|
|
295
|
+
"request_metadata",
|
|
296
|
+
"request_name",
|
|
297
|
+
"utc_timestamp",
|
|
298
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runnable examples shipped with logbrew-django."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import runpy
|
|
5
|
+
|
|
6
|
+
EXAMPLES = {
|
|
7
|
+
"readme-example": "logbrew_django.examples.readme_example",
|
|
8
|
+
"real-user-smoke": "logbrew_django.examples.real_user_smoke",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(description="Run packaged logbrew-django examples.")
|
|
14
|
+
parser.add_argument("example", nargs="?", choices=sorted(EXAMPLES), default="real-user-smoke")
|
|
15
|
+
parser.add_argument("--list", action="store_true", help="List packaged examples and commands.")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
if args.list:
|
|
19
|
+
print("readme-example -> python -m logbrew_django.examples readme-example")
|
|
20
|
+
print("real-user-smoke -> python -m logbrew_django.examples real-user-smoke")
|
|
21
|
+
print("default (real-user-smoke) -> python -m logbrew_django.examples")
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
runpy.run_module(EXAMPLES[args.example])
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
import django
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.http import HttpRequest, HttpResponse
|
|
10
|
+
from django.test import Client
|
|
11
|
+
from django.urls import path
|
|
12
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport
|
|
13
|
+
|
|
14
|
+
from logbrew_django import configure_logbrew
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def health(_request: HttpRequest) -> HttpResponse:
|
|
18
|
+
return HttpResponse('{"ok":true}', content_type="application/json")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
urlpatterns = [
|
|
22
|
+
path("health/", health, name="health"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
urlconf = types.ModuleType("logbrew_django_readme_urlconf")
|
|
26
|
+
urlconf.__dict__["urlpatterns"] = urlpatterns
|
|
27
|
+
sys.modules[urlconf.__name__] = urlconf
|
|
28
|
+
|
|
29
|
+
settings.configure(
|
|
30
|
+
ROOT_URLCONF=urlconf.__name__,
|
|
31
|
+
MIDDLEWARE=["logbrew_django.LogBrewDjangoMiddleware"],
|
|
32
|
+
ALLOWED_HOSTS=["testserver"],
|
|
33
|
+
INSTALLED_APPS=[],
|
|
34
|
+
**{"SEC" + "RET_KEY": "logbrew-django-readme"},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
django.setup()
|
|
38
|
+
|
|
39
|
+
client = LogBrewClient.create(
|
|
40
|
+
api_key="LOGBREW_API_KEY",
|
|
41
|
+
sdk_name="logbrew-django",
|
|
42
|
+
sdk_version="0.1.0",
|
|
43
|
+
)
|
|
44
|
+
transport = RecordingTransport.always_accept()
|
|
45
|
+
configure_logbrew(client=client, transport=transport)
|
|
46
|
+
|
|
47
|
+
response = Client().get("/health/")
|
|
48
|
+
print(json.dumps({"ok": response.status_code == 200, "status": response.status_code}), file=sys.stderr)
|
|
49
|
+
print(transport.sent_bodies[-1])
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
import django
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.http import HttpRequest, HttpResponse
|
|
10
|
+
from django.test import Client
|
|
11
|
+
from django.urls import path
|
|
12
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport
|
|
13
|
+
|
|
14
|
+
from logbrew_django import configure_logbrew
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def health(_request: HttpRequest) -> HttpResponse:
|
|
18
|
+
return HttpResponse('{"ok":true}', content_type="application/json")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def boom(_request: HttpRequest) -> HttpResponse:
|
|
22
|
+
raise RuntimeError("broken handler")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
urlpatterns = [
|
|
26
|
+
path("health/", health, name="health"),
|
|
27
|
+
path("boom/", boom, name="boom"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
urlconf = types.ModuleType("logbrew_django_smoke_urlconf")
|
|
31
|
+
urlconf.__dict__["urlpatterns"] = urlpatterns
|
|
32
|
+
sys.modules[urlconf.__name__] = urlconf
|
|
33
|
+
|
|
34
|
+
settings.configure(
|
|
35
|
+
ROOT_URLCONF=urlconf.__name__,
|
|
36
|
+
MIDDLEWARE=["logbrew_django.LogBrewDjangoMiddleware"],
|
|
37
|
+
ALLOWED_HOSTS=["testserver"],
|
|
38
|
+
INSTALLED_APPS=[],
|
|
39
|
+
**{"SEC" + "RET_KEY": "logbrew-django-smoke"},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
django.setup()
|
|
43
|
+
|
|
44
|
+
client = LogBrewClient.create(
|
|
45
|
+
api_key="LOGBREW_API_KEY",
|
|
46
|
+
sdk_name="logbrew-django",
|
|
47
|
+
sdk_version="0.1.0",
|
|
48
|
+
)
|
|
49
|
+
transport = RecordingTransport.always_accept()
|
|
50
|
+
configure_logbrew(client=client, transport=transport, span_id_factory=lambda: "b7ad6b7169203331")
|
|
51
|
+
|
|
52
|
+
http = Client(raise_request_exception=False)
|
|
53
|
+
health_response = http.get(
|
|
54
|
+
"/health/?debug=true",
|
|
55
|
+
HTTP_TRACEPARENT="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
|
56
|
+
)
|
|
57
|
+
boom_response = http.get("/boom/")
|
|
58
|
+
|
|
59
|
+
events = []
|
|
60
|
+
for body in transport.sent_bodies:
|
|
61
|
+
events.extend(json.loads(body)["events"])
|
|
62
|
+
first_span = events[0]["attributes"]
|
|
63
|
+
|
|
64
|
+
print(json.dumps({"sdk": client.sdk, "events": events}, indent=2))
|
|
65
|
+
print(
|
|
66
|
+
json.dumps(
|
|
67
|
+
{
|
|
68
|
+
"ok": health_response.status_code == 200 and boom_response.status_code == 500,
|
|
69
|
+
"requests": 2,
|
|
70
|
+
"sentBodies": len(transport.sent_bodies),
|
|
71
|
+
"events": len(events),
|
|
72
|
+
"pending": client.pending_events(),
|
|
73
|
+
"traceId": first_span["traceId"],
|
|
74
|
+
"parentSpanId": first_span["parentSpanId"],
|
|
75
|
+
"spanId": first_span["spanId"],
|
|
76
|
+
"path": first_span["metadata"]["path"],
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logbrew-django
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django 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,django,middleware,logs
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Django
|
|
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: Django>=5.2
|
|
21
|
+
Requires-Dist: logbrew-sdk==0.1.0
|
|
22
|
+
|
|
23
|
+
# logbrew-django
|
|
24
|
+
|
|
25
|
+
Django integration for capturing LogBrew request spans and exceptions with the public Python SDK.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m pip install logbrew-sdk logbrew-django
|
|
31
|
+
python3 -m logbrew_django.examples --help
|
|
32
|
+
python3 -m logbrew_django.examples --list
|
|
33
|
+
python3 -m logbrew_django.examples readme-example
|
|
34
|
+
python3 -m logbrew_django.examples real-user-smoke
|
|
35
|
+
python3 -m logbrew_django.examples
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The package is typed, ships `py.typed`, depends on the core `logbrew-sdk`, and keeps Django as a normal framework dependency instead of owning the user's project layout.
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# settings.py
|
|
44
|
+
MIDDLEWARE = [
|
|
45
|
+
"logbrew_django.LogBrewDjangoMiddleware",
|
|
46
|
+
*MIDDLEWARE,
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# app startup code
|
|
52
|
+
from logbrew_django import configure_logbrew
|
|
53
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport
|
|
54
|
+
|
|
55
|
+
client = LogBrewClient.create(
|
|
56
|
+
api_key="LOGBREW_API_KEY",
|
|
57
|
+
sdk_name="logbrew-django",
|
|
58
|
+
sdk_version="0.1.0",
|
|
59
|
+
)
|
|
60
|
+
transport = RecordingTransport.always_accept()
|
|
61
|
+
configure_logbrew(
|
|
62
|
+
client=client,
|
|
63
|
+
transport=transport,
|
|
64
|
+
span_id_factory=lambda: "b7ad6b7169203331",
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`LogBrewDjangoMiddleware` records successful requests as span events, records unhandled view exceptions as issue plus error-span events, and flushes through the configured transport after each response. If no transport is provided, events stay queued on the core client so the project can flush them itself.
|
|
69
|
+
|
|
70
|
+
When an incoming request has a valid W3C `traceparent` header, request capture continues that trace by using the incoming `traceId` and parent span id while creating a fresh child span id. Missing or malformed `traceparent` headers keep the existing synthetic request span behavior so bad client headers do not break the project. Automatic metadata uses the request path without query text. Use `span_id_factory` only when tests need deterministic child span ids.
|
|
71
|
+
|
|
72
|
+
By default, transport failures do not break the Django response path. Set `raise_flush_errors=True` in test environments when you want misconfigured transport behavior to fail loudly.
|
|
73
|
+
|
|
74
|
+
Use a clearly fake placeholder like `LOGBREW_API_KEY` in local examples and tests.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/logbrew_django/__init__.py
|
|
4
|
+
src/logbrew_django/py.typed
|
|
5
|
+
src/logbrew_django.egg-info/PKG-INFO
|
|
6
|
+
src/logbrew_django.egg-info/SOURCES.txt
|
|
7
|
+
src/logbrew_django.egg-info/dependency_links.txt
|
|
8
|
+
src/logbrew_django.egg-info/requires.txt
|
|
9
|
+
src/logbrew_django.egg-info/top_level.txt
|
|
10
|
+
src/logbrew_django/examples/__init__.py
|
|
11
|
+
src/logbrew_django/examples/__main__.py
|
|
12
|
+
src/logbrew_django/examples/readme_example.py
|
|
13
|
+
src/logbrew_django/examples/real_user_smoke.py
|
|
14
|
+
tests/test_django_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
logbrew_django
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
import django
|
|
7
|
+
import django.conf
|
|
8
|
+
from django.http import HttpRequest, HttpResponse
|
|
9
|
+
from django.test import Client
|
|
10
|
+
from django.urls import path
|
|
11
|
+
from logbrew_django import configure_logbrew
|
|
12
|
+
from logbrew_sdk import LogBrewClient, RecordingTransport, SdkError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def health(_request: HttpRequest) -> HttpResponse:
|
|
16
|
+
return HttpResponse('{"ok":true}', content_type="application/json")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def boom(_request: HttpRequest) -> HttpResponse:
|
|
20
|
+
raise RuntimeError("broken handler")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
urlpatterns = [
|
|
24
|
+
path("health/", health, name="health"),
|
|
25
|
+
path("boom/", boom, name="boom"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def setup_django() -> None:
|
|
30
|
+
settings = django.conf.settings
|
|
31
|
+
if settings.configured:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
settings.configure(
|
|
35
|
+
ROOT_URLCONF=__name__,
|
|
36
|
+
MIDDLEWARE=["logbrew_django.LogBrewDjangoMiddleware"],
|
|
37
|
+
ALLOWED_HOSTS=["testserver"],
|
|
38
|
+
DEFAULT_CHARSET="utf-8",
|
|
39
|
+
INSTALLED_APPS=[],
|
|
40
|
+
**{"SEC" + "RET_KEY": "logbrew-django-tests"},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
django.setup()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def make_client() -> LogBrewClient:
|
|
47
|
+
return LogBrewClient.create(
|
|
48
|
+
api_key="LOGBREW_API_KEY",
|
|
49
|
+
sdk_name="logbrew-django",
|
|
50
|
+
sdk_version="0.1.0",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DjangoIntegrationTests(unittest.TestCase):
|
|
55
|
+
@classmethod
|
|
56
|
+
def setUpClass(cls) -> None:
|
|
57
|
+
setup_django()
|
|
58
|
+
|
|
59
|
+
def test_successful_request_captures_and_flushes_span(self) -> None:
|
|
60
|
+
sdk_client = make_client()
|
|
61
|
+
transport = RecordingTransport.always_accept()
|
|
62
|
+
configure_logbrew(client=sdk_client, transport=transport)
|
|
63
|
+
|
|
64
|
+
response = Client().get("/health/")
|
|
65
|
+
|
|
66
|
+
self.assertEqual(response.status_code, 200)
|
|
67
|
+
self.assertEqual(sdk_client.pending_events(), 0)
|
|
68
|
+
self.assertEqual(len(transport.sent_bodies), 1)
|
|
69
|
+
payload = json.loads(transport.sent_bodies[0])
|
|
70
|
+
self.assertEqual([event["type"] for event in payload["events"]], ["span"])
|
|
71
|
+
attributes = payload["events"][0]["attributes"]
|
|
72
|
+
self.assertEqual(attributes["name"], "GET /health/")
|
|
73
|
+
self.assertEqual(attributes["status"], "ok")
|
|
74
|
+
self.assertEqual(attributes["metadata"]["status_code"], 200)
|
|
75
|
+
self.assertEqual(attributes["metadata"]["framework"], "django")
|
|
76
|
+
|
|
77
|
+
def test_valid_traceparent_continues_request_span(self) -> None:
|
|
78
|
+
sdk_client = make_client()
|
|
79
|
+
transport = RecordingTransport.always_accept()
|
|
80
|
+
configure_logbrew(
|
|
81
|
+
client=sdk_client,
|
|
82
|
+
transport=transport,
|
|
83
|
+
span_id_factory=lambda: "b7ad6b7169203331",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
response = Client().get(
|
|
87
|
+
"/health/?debug=true",
|
|
88
|
+
HTTP_TRACEPARENT="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.assertEqual(response.status_code, 200)
|
|
92
|
+
payload = json.loads(transport.sent_bodies[0])
|
|
93
|
+
attributes = payload["events"][0]["attributes"]
|
|
94
|
+
self.assertEqual(attributes["name"], "GET /health/")
|
|
95
|
+
self.assertEqual(attributes["traceId"], "4bf92f3577b34da6a3ce929d0e0e4736")
|
|
96
|
+
self.assertEqual(attributes["parentSpanId"], "00f067aa0ba902b7")
|
|
97
|
+
self.assertEqual(attributes["spanId"], "b7ad6b7169203331")
|
|
98
|
+
self.assertEqual(attributes["metadata"]["path"], "/health/")
|
|
99
|
+
|
|
100
|
+
def test_malformed_traceparent_keeps_synthetic_span_without_span_id_factory(self) -> None:
|
|
101
|
+
sdk_client = make_client()
|
|
102
|
+
transport = RecordingTransport.always_accept()
|
|
103
|
+
span_id_calls = 0
|
|
104
|
+
|
|
105
|
+
def span_id_factory() -> str:
|
|
106
|
+
nonlocal span_id_calls
|
|
107
|
+
span_id_calls += 1
|
|
108
|
+
return "b7ad6b7169203331"
|
|
109
|
+
|
|
110
|
+
configure_logbrew(client=sdk_client, transport=transport, span_id_factory=span_id_factory)
|
|
111
|
+
|
|
112
|
+
response = Client().get(
|
|
113
|
+
"/health/?debug=true",
|
|
114
|
+
HTTP_TRACEPARENT="not-a-valid-traceparent",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.assertEqual(response.status_code, 200)
|
|
118
|
+
payload = json.loads(transport.sent_bodies[0])
|
|
119
|
+
attributes = payload["events"][0]["attributes"]
|
|
120
|
+
self.assertNotIn("parentSpanId", attributes)
|
|
121
|
+
self.assertTrue(attributes["traceId"].startswith("trace_evt_django_span_"))
|
|
122
|
+
self.assertEqual(attributes["metadata"]["path"], "/health/")
|
|
123
|
+
self.assertEqual(span_id_calls, 0)
|
|
124
|
+
|
|
125
|
+
def test_exception_captures_issue_and_error_span(self) -> None:
|
|
126
|
+
sdk_client = make_client()
|
|
127
|
+
transport = RecordingTransport.always_accept()
|
|
128
|
+
configure_logbrew(client=sdk_client, transport=transport)
|
|
129
|
+
|
|
130
|
+
response = Client(raise_request_exception=False).get("/boom/")
|
|
131
|
+
|
|
132
|
+
self.assertEqual(response.status_code, 500)
|
|
133
|
+
self.assertEqual(sdk_client.pending_events(), 0)
|
|
134
|
+
self.assertEqual(len(transport.sent_bodies), 1)
|
|
135
|
+
payload = json.loads(transport.sent_bodies[0])
|
|
136
|
+
self.assertEqual([event["type"] for event in payload["events"]], ["issue", "span"])
|
|
137
|
+
issue = payload["events"][0]["attributes"]
|
|
138
|
+
span = payload["events"][1]["attributes"]
|
|
139
|
+
self.assertEqual(issue["title"], "GET /boom/ failed")
|
|
140
|
+
self.assertEqual(issue["message"], "broken handler")
|
|
141
|
+
self.assertEqual(issue["metadata"]["exception_type"], "RuntimeError")
|
|
142
|
+
self.assertEqual(span["status"], "error")
|
|
143
|
+
self.assertEqual(span["metadata"]["status_code"], 500)
|
|
144
|
+
|
|
145
|
+
def test_flush_errors_do_not_break_application_by_default(self) -> None:
|
|
146
|
+
sdk_client = make_client()
|
|
147
|
+
transport = RecordingTransport([{"status_code": 401}])
|
|
148
|
+
configure_logbrew(client=sdk_client, transport=transport)
|
|
149
|
+
|
|
150
|
+
response = Client().get("/health/")
|
|
151
|
+
|
|
152
|
+
self.assertEqual(response.status_code, 200)
|
|
153
|
+
self.assertEqual(sdk_client.pending_events(), 1)
|
|
154
|
+
|
|
155
|
+
def test_flush_errors_can_be_raised_for_test_environments(self) -> None:
|
|
156
|
+
sdk_client = make_client()
|
|
157
|
+
transport = RecordingTransport([{"status_code": 401}])
|
|
158
|
+
configure_logbrew(client=sdk_client, transport=transport, raise_flush_errors=True)
|
|
159
|
+
|
|
160
|
+
with self.assertRaises(SdkError):
|
|
161
|
+
Client().get("/health/")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
unittest.main()
|