jararaca 0.3.11a16__py3-none-any.whl → 0.4.0a5__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.
- README.md +121 -0
- jararaca/__init__.py +184 -12
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +272 -47
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +33 -67
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
- jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +850 -383
- jararaca/microservice.py +110 -1
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +202 -11
- jararaca/persistence/base.py +38 -2
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +50 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +34 -25
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1074 -173
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +65 -39
- jararaca/utils/retry.py +10 -3
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
jararaca/microservice.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
1
6
|
import inspect
|
|
2
7
|
import logging
|
|
3
8
|
from contextlib import contextmanager, suppress
|
|
@@ -10,6 +15,7 @@ from typing import (
|
|
|
10
15
|
Any,
|
|
11
16
|
AsyncContextManager,
|
|
12
17
|
Callable,
|
|
18
|
+
Coroutine,
|
|
13
19
|
Generator,
|
|
14
20
|
Literal,
|
|
15
21
|
Protocol,
|
|
@@ -19,7 +25,7 @@ from typing import (
|
|
|
19
25
|
runtime_checkable,
|
|
20
26
|
)
|
|
21
27
|
|
|
22
|
-
from fastapi import Request, WebSocket
|
|
28
|
+
from fastapi import Request, Response, WebSocket
|
|
23
29
|
|
|
24
30
|
from jararaca.core.providers import ProviderSpec, T, Token
|
|
25
31
|
from jararaca.messagebus import MessageOf
|
|
@@ -34,6 +40,7 @@ if TYPE_CHECKING:
|
|
|
34
40
|
|
|
35
41
|
@dataclass
|
|
36
42
|
class SchedulerTransactionData:
|
|
43
|
+
task_name: str
|
|
37
44
|
triggered_at: datetime
|
|
38
45
|
scheduled_to: datetime
|
|
39
46
|
cron_expression: str
|
|
@@ -43,6 +50,7 @@ class SchedulerTransactionData:
|
|
|
43
50
|
@dataclass
|
|
44
51
|
class HttpTransactionData:
|
|
45
52
|
request: Request
|
|
53
|
+
response: Response
|
|
46
54
|
context_type: Literal["http"] = "http"
|
|
47
55
|
|
|
48
56
|
|
|
@@ -50,6 +58,7 @@ class HttpTransactionData:
|
|
|
50
58
|
class MessageBusTransactionData:
|
|
51
59
|
topic: str
|
|
52
60
|
message: MessageOf[Message]
|
|
61
|
+
message_type: Type[Message]
|
|
53
62
|
context_type: Literal["message_bus"] = "message_bus"
|
|
54
63
|
|
|
55
64
|
|
|
@@ -59,6 +68,8 @@ class WebSocketTransactionData:
|
|
|
59
68
|
context_type: Literal["websocket"] = "websocket"
|
|
60
69
|
|
|
61
70
|
|
|
71
|
+
APP_TYPE = Literal["http", "worker", "beat"]
|
|
72
|
+
|
|
62
73
|
TransactionData = (
|
|
63
74
|
MessageBusTransactionData
|
|
64
75
|
| HttpTransactionData
|
|
@@ -325,6 +336,104 @@ def provide_container(container: Container) -> Generator[None, None, None]:
|
|
|
325
336
|
current_container_ctx.reset(token)
|
|
326
337
|
|
|
327
338
|
|
|
339
|
+
class ShutdownState(Protocol):
|
|
340
|
+
|
|
341
|
+
def request_shutdown(self) -> None: ...
|
|
342
|
+
|
|
343
|
+
def is_shutdown_requested(self) -> bool: ...
|
|
344
|
+
|
|
345
|
+
async def wait_for_shutdown(self) -> None: ...
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
shutdown_state_ctx = ContextVar[ShutdownState]("shutdown_state")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def is_shutting_down() -> bool:
|
|
352
|
+
"""
|
|
353
|
+
Check if the application is in the process of shutting down.
|
|
354
|
+
"""
|
|
355
|
+
return shutdown_state_ctx.get().is_shutdown_requested()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def request_shutdown() -> None:
|
|
359
|
+
"""
|
|
360
|
+
Request the application to shut down.
|
|
361
|
+
This will set the shutdown event, allowing the application to gracefully shut down.
|
|
362
|
+
"""
|
|
363
|
+
shutdown_state_ctx.get().request_shutdown()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def wait_for_shutdown() -> None:
|
|
367
|
+
"""
|
|
368
|
+
Wait for the shutdown event to be set.
|
|
369
|
+
This function will block until a shutdown is requested.
|
|
370
|
+
"""
|
|
371
|
+
await shutdown_state_ctx.get().wait_for_shutdown()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def shutdown_race(*concurrent_tasks: Coroutine[Any, Any, Any]) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Wait for either a shutdown request or any of the provided tasks to complete.
|
|
377
|
+
This function will return as soon as a shutdown is requested or any task finishes.
|
|
378
|
+
Returns True if shutdown was requested, False if a task completed first.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
tasks = [asyncio.create_task(t) for t in concurrent_tasks + (wait_for_shutdown(),)]
|
|
382
|
+
|
|
383
|
+
_, pending = await asyncio.wait(
|
|
384
|
+
tasks,
|
|
385
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
for task in pending:
|
|
389
|
+
task.cancel()
|
|
390
|
+
|
|
391
|
+
return is_shutting_down()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@contextmanager
|
|
395
|
+
def provide_shutdown_state(
|
|
396
|
+
state: ShutdownState,
|
|
397
|
+
) -> Generator[None, None, None]:
|
|
398
|
+
"""
|
|
399
|
+
Context manager to provide the shutdown state.
|
|
400
|
+
This is used to manage the shutdown event for the application.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
token = shutdown_state_ctx.set(state)
|
|
404
|
+
try:
|
|
405
|
+
yield
|
|
406
|
+
finally:
|
|
407
|
+
with suppress(ValueError):
|
|
408
|
+
shutdown_state_ctx.reset(token)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
app_type_ctx = ContextVar[APP_TYPE]("app_type")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def use_app_type() -> APP_TYPE:
|
|
415
|
+
"""
|
|
416
|
+
Returns the current application type.
|
|
417
|
+
This function is used to access the application type in the context of an application transaction.
|
|
418
|
+
If no context is set, it raises a LookupError.
|
|
419
|
+
"""
|
|
420
|
+
return app_type_ctx.get()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@contextmanager
|
|
424
|
+
def providing_app_type(app_type: APP_TYPE) -> Generator[None, None, None]:
|
|
425
|
+
"""
|
|
426
|
+
Context manager to provide the application type.
|
|
427
|
+
This is used to set the application type for the current transaction.
|
|
428
|
+
"""
|
|
429
|
+
token = app_type_ctx.set(app_type)
|
|
430
|
+
try:
|
|
431
|
+
yield
|
|
432
|
+
finally:
|
|
433
|
+
with suppress(ValueError):
|
|
434
|
+
app_type_ctx.reset(token)
|
|
435
|
+
|
|
436
|
+
|
|
328
437
|
__all__ = [
|
|
329
438
|
"AppTransactionContext",
|
|
330
439
|
"AppInterceptor",
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
1
6
|
from contextlib import contextmanager, suppress
|
|
2
7
|
from contextvars import ContextVar
|
|
3
8
|
from functools import wraps
|
|
@@ -8,23 +13,72 @@ from typing import (
|
|
|
8
13
|
Callable,
|
|
9
14
|
ContextManager,
|
|
10
15
|
Generator,
|
|
11
|
-
|
|
16
|
+
Literal,
|
|
17
|
+
Mapping,
|
|
12
18
|
Protocol,
|
|
19
|
+
Sequence,
|
|
13
20
|
TypeVar,
|
|
21
|
+
Union,
|
|
14
22
|
)
|
|
15
23
|
|
|
16
24
|
from jararaca.microservice import AppTransactionContext
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
|
|
27
|
+
|
|
28
|
+
AttributeValue = Union[
|
|
29
|
+
str,
|
|
30
|
+
bool,
|
|
31
|
+
int,
|
|
32
|
+
float,
|
|
33
|
+
Sequence[str],
|
|
34
|
+
Sequence[bool],
|
|
35
|
+
Sequence[int],
|
|
36
|
+
Sequence[float],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
AttributeMap = Mapping[str, AttributeValue]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TracingSpan(Protocol): ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TracingSpanContext(Protocol): ...
|
|
20
46
|
|
|
21
47
|
|
|
22
48
|
class TracingContextProvider(Protocol):
|
|
23
49
|
|
|
24
|
-
def
|
|
25
|
-
self, trace_name: str, context_attributes:
|
|
50
|
+
def start_span_context(
|
|
51
|
+
self, trace_name: str, context_attributes: AttributeMap | None
|
|
26
52
|
) -> ContextManager[Any]: ...
|
|
27
53
|
|
|
54
|
+
def add_event(
|
|
55
|
+
self,
|
|
56
|
+
event_name: str,
|
|
57
|
+
event_attributes: AttributeMap | None = None,
|
|
58
|
+
) -> None: ...
|
|
59
|
+
|
|
60
|
+
def set_span_status(self, status_code: Literal["OK", "ERROR", "UNSET"]) -> None: ...
|
|
61
|
+
|
|
62
|
+
def record_exception(
|
|
63
|
+
self,
|
|
64
|
+
exception: Exception,
|
|
65
|
+
attributes: AttributeMap | None = None,
|
|
66
|
+
escaped: bool = False,
|
|
67
|
+
) -> None: ...
|
|
68
|
+
|
|
69
|
+
def set_span_attribute(
|
|
70
|
+
self,
|
|
71
|
+
key: str,
|
|
72
|
+
value: AttributeValue,
|
|
73
|
+
) -> None: ...
|
|
74
|
+
|
|
75
|
+
def update_span_name(self, new_name: str) -> None: ...
|
|
76
|
+
|
|
77
|
+
def add_link(self, span_context: TracingSpanContext) -> None: ...
|
|
78
|
+
|
|
79
|
+
def get_current_span(self) -> TracingSpan | None: ...
|
|
80
|
+
def get_current_span_context(self) -> TracingSpanContext | None: ...
|
|
81
|
+
|
|
28
82
|
|
|
29
83
|
class TracingContextProviderFactory(Protocol):
|
|
30
84
|
|
|
@@ -76,22 +130,125 @@ class TracedFunc:
|
|
|
76
130
|
|
|
77
131
|
def __call__(
|
|
78
132
|
self,
|
|
79
|
-
decorated:
|
|
80
|
-
) ->
|
|
133
|
+
decorated: F,
|
|
134
|
+
) -> F:
|
|
81
135
|
|
|
82
136
|
@wraps(decorated)
|
|
83
137
|
async def wrapper(
|
|
84
|
-
*args:
|
|
85
|
-
**kwargs:
|
|
86
|
-
) ->
|
|
138
|
+
*args: Any,
|
|
139
|
+
**kwargs: Any,
|
|
140
|
+
) -> Any:
|
|
87
141
|
|
|
88
142
|
if ctx_provider := get_tracing_ctx_provider():
|
|
89
|
-
with ctx_provider(
|
|
143
|
+
with ctx_provider.start_span_context(
|
|
90
144
|
self.trace_name,
|
|
91
|
-
self.trace_mapper(**kwargs),
|
|
145
|
+
self.trace_mapper(*args, **kwargs),
|
|
92
146
|
):
|
|
93
147
|
return await decorated(*args, **kwargs)
|
|
94
148
|
|
|
95
149
|
return await decorated(*args, **kwargs)
|
|
96
150
|
|
|
97
|
-
return wrapper
|
|
151
|
+
return wrapper # type: ignore[return-value]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
C = TypeVar("C", bound=type)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TracedClass:
|
|
158
|
+
"""
|
|
159
|
+
Class decorator that automatically applies tracing to all async methods in a class.
|
|
160
|
+
|
|
161
|
+
Usage:
|
|
162
|
+
@TracedClass()
|
|
163
|
+
class MyService:
|
|
164
|
+
async def method1(self) -> str:
|
|
165
|
+
return "hello"
|
|
166
|
+
|
|
167
|
+
async def method2(self, x: int) -> int:
|
|
168
|
+
return x * 2
|
|
169
|
+
|
|
170
|
+
def sync_method(self) -> str: # Not traced
|
|
171
|
+
return "sync"
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
trace_name_prefix: str | None = None,
|
|
177
|
+
trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
|
|
178
|
+
include_private: bool = False,
|
|
179
|
+
exclude_methods: set[str] | None = None,
|
|
180
|
+
):
|
|
181
|
+
"""
|
|
182
|
+
Initialize the TracedClass decorator.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
trace_name_prefix: Prefix for trace names. If None, uses class name.
|
|
186
|
+
trace_mapper: Function to map method arguments to trace attributes.
|
|
187
|
+
include_private: Whether to trace private methods (starting with _).
|
|
188
|
+
exclude_methods: Set of method names to exclude from tracing.
|
|
189
|
+
"""
|
|
190
|
+
self.trace_name_prefix = trace_name_prefix
|
|
191
|
+
self.trace_mapper = trace_mapper
|
|
192
|
+
self.include_private = include_private
|
|
193
|
+
self.exclude_methods = exclude_methods or set()
|
|
194
|
+
|
|
195
|
+
def __call__(self, cls: C) -> C:
|
|
196
|
+
"""Apply tracing to all async methods in the class."""
|
|
197
|
+
|
|
198
|
+
# Use class name as prefix if not provided
|
|
199
|
+
trace_prefix = self.trace_name_prefix or cls.__name__
|
|
200
|
+
|
|
201
|
+
# Get all methods in the class
|
|
202
|
+
for name, method in inspect.getmembers_static(
|
|
203
|
+
cls, predicate=inspect.isfunction
|
|
204
|
+
):
|
|
205
|
+
# Skip if method should be excluded
|
|
206
|
+
if name in self.exclude_methods:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Skip private methods unless explicitly included
|
|
210
|
+
if name.startswith("_") and not self.include_private:
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Only trace async methods
|
|
214
|
+
if inspect.iscoroutinefunction(method):
|
|
215
|
+
trace_name = f"{trace_prefix}.{name}"
|
|
216
|
+
traced_method = TracedFunc(trace_name, self.trace_mapper)(method)
|
|
217
|
+
setattr(cls, name, traced_method)
|
|
218
|
+
|
|
219
|
+
return cls
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def traced_class(
|
|
223
|
+
trace_name_prefix: str | None = None,
|
|
224
|
+
trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
|
|
225
|
+
include_private: bool = False,
|
|
226
|
+
exclude_methods: set[str] | None = None,
|
|
227
|
+
) -> Callable[[C], C]:
|
|
228
|
+
"""
|
|
229
|
+
Functional interface for TracedClass decorator.
|
|
230
|
+
|
|
231
|
+
Usage:
|
|
232
|
+
@traced_class(trace_name_prefix="MyService")
|
|
233
|
+
class MyService:
|
|
234
|
+
async def method1(self) -> str:
|
|
235
|
+
return "hello"
|
|
236
|
+
"""
|
|
237
|
+
return TracedClass(
|
|
238
|
+
trace_name_prefix=trace_name_prefix,
|
|
239
|
+
trace_mapper=trace_mapper,
|
|
240
|
+
include_private=include_private,
|
|
241
|
+
exclude_methods=exclude_methods,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = [
|
|
246
|
+
"TracingContextProvider",
|
|
247
|
+
"TracingContextProviderFactory",
|
|
248
|
+
"provide_tracing_ctx_provider",
|
|
249
|
+
"get_tracing_ctx_provider",
|
|
250
|
+
"default_trace_mapper",
|
|
251
|
+
"TracedFunc",
|
|
252
|
+
"TracedClass",
|
|
253
|
+
"traced_class",
|
|
254
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, Request, Response
|
|
6
|
+
from fastapi.exception_handlers import (
|
|
7
|
+
http_exception_handler,
|
|
8
|
+
request_validation_exception_handler,
|
|
9
|
+
)
|
|
10
|
+
from fastapi.exceptions import RequestValidationError
|
|
11
|
+
from fastapi.responses import JSONResponse
|
|
12
|
+
from starlette.exceptions import HTTPException
|
|
13
|
+
|
|
14
|
+
from jararaca.observability.constants import TRACEPARENT_KEY
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup_fastapi_exception_handler(
|
|
18
|
+
app: FastAPI, trace_header_name: str = "traceparent"
|
|
19
|
+
) -> None:
|
|
20
|
+
async def base_http_exception_handler(
|
|
21
|
+
request: Request, exc: HTTPException | RequestValidationError
|
|
22
|
+
) -> JSONResponse | Response:
|
|
23
|
+
|
|
24
|
+
if isinstance(exc, RequestValidationError):
|
|
25
|
+
response = await request_validation_exception_handler(request, exc)
|
|
26
|
+
response.headers[trace_header_name] = request.scope.get(TRACEPARENT_KEY, "")
|
|
27
|
+
return response
|
|
28
|
+
else:
|
|
29
|
+
err_response = await http_exception_handler(request, exc)
|
|
30
|
+
|
|
31
|
+
err_response.headers[trace_header_name] = request.scope.get(
|
|
32
|
+
TRACEPARENT_KEY, ""
|
|
33
|
+
)
|
|
34
|
+
return err_response
|
|
35
|
+
|
|
36
|
+
app.exception_handlers[HTTPException] = base_http_exception_handler
|
|
37
|
+
app.exception_handlers[RequestValidationError] = base_http_exception_handler
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import typing
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Any, Generator, Literal
|
|
9
|
+
|
|
10
|
+
from jararaca.observability.decorators import (
|
|
11
|
+
AttributeMap,
|
|
12
|
+
AttributeValue,
|
|
13
|
+
TracingContextProvider,
|
|
14
|
+
TracingSpan,
|
|
15
|
+
TracingSpanContext,
|
|
16
|
+
get_tracing_ctx_provider,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def start_span(
|
|
22
|
+
name: str,
|
|
23
|
+
attributes: AttributeMap | None = None,
|
|
24
|
+
) -> Generator[None, Any, None]:
|
|
25
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
26
|
+
with trace_context_provider.start_span_context(
|
|
27
|
+
trace_name=name, context_attributes=attributes
|
|
28
|
+
):
|
|
29
|
+
yield
|
|
30
|
+
else:
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def spawn_trace(
|
|
35
|
+
name: str,
|
|
36
|
+
attributes: AttributeMap | None = None,
|
|
37
|
+
) -> typing.ContextManager[None]:
|
|
38
|
+
logging.warning(
|
|
39
|
+
"spawn_trace is deprecated, use start_span as context manager instead."
|
|
40
|
+
)
|
|
41
|
+
return start_span(name=name, attributes=attributes)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def add_event(
|
|
45
|
+
name: str,
|
|
46
|
+
attributes: AttributeMap | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
|
|
49
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
50
|
+
trace_context_provider.add_event(
|
|
51
|
+
event_name=name,
|
|
52
|
+
event_attributes=attributes,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_span_status(status_code: Literal["OK", "ERROR", "UNSET"]) -> None:
|
|
57
|
+
|
|
58
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
59
|
+
trace_context_provider.set_span_status(status_code=status_code)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def record_exception(
|
|
63
|
+
exception: Exception,
|
|
64
|
+
attributes: AttributeMap | None = None,
|
|
65
|
+
escaped: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
|
|
68
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
69
|
+
trace_context_provider.record_exception(
|
|
70
|
+
exception=exception,
|
|
71
|
+
attributes=attributes,
|
|
72
|
+
escaped=escaped,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_span_attribute(
|
|
77
|
+
key: str,
|
|
78
|
+
value: AttributeValue,
|
|
79
|
+
) -> None:
|
|
80
|
+
|
|
81
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
82
|
+
trace_context_provider.set_span_attribute(
|
|
83
|
+
key=key,
|
|
84
|
+
value=value,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_tracing_provider() -> TracingContextProvider | None:
|
|
89
|
+
return get_tracing_ctx_provider()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_current_span_context() -> TracingSpanContext | None:
|
|
93
|
+
|
|
94
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
95
|
+
return trace_context_provider.get_current_span_context()
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_current_span() -> TracingSpan | None:
|
|
100
|
+
|
|
101
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
102
|
+
return trace_context_provider.get_current_span()
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_span_link(span_context: TracingSpanContext) -> None:
|
|
107
|
+
|
|
108
|
+
if trace_context_provider := get_tracing_ctx_provider():
|
|
109
|
+
trace_context_provider.add_link(span_context=span_context)
|