jararaca 0.3.18__tar.gz → 0.3.19__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.

Potentially problematic release.


This version of jararaca might be problematic. Click here for more details.

Files changed (94) hide show
  1. {jararaca-0.3.18 → jararaca-0.3.19}/PKG-INFO +4 -4
  2. {jararaca-0.3.18 → jararaca-0.3.19}/pyproject.toml +4 -4
  3. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/__init__.py +10 -0
  4. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/core/uow.py +33 -5
  5. jararaca-0.3.19/src/jararaca/messagebus/implicit_headers.py +45 -0
  6. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +6 -1
  7. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/worker.py +6 -1
  8. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/microservice.py +3 -1
  9. jararaca-0.3.19/src/jararaca/observability/decorators.py +213 -0
  10. jararaca-0.3.19/src/jararaca/observability/hooks.py +20 -0
  11. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/observability/providers/otel.py +62 -9
  12. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/server.py +28 -4
  13. jararaca-0.3.18/src/jararaca/observability/decorators.py +0 -97
  14. {jararaca-0.3.18 → jararaca-0.3.19}/LICENSE +0 -0
  15. {jararaca-0.3.18 → jararaca-0.3.19}/README.md +0 -0
  16. {jararaca-0.3.18 → jararaca-0.3.19}/docs/CNAME +0 -0
  17. {jararaca-0.3.18 → jararaca-0.3.19}/docs/architecture.md +0 -0
  18. {jararaca-0.3.18 → jararaca-0.3.19}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg +0 -0
  19. {jararaca-0.3.18 → jararaca-0.3.19}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.webp +0 -0
  20. {jararaca-0.3.18 → jararaca-0.3.19}/docs/assets/tracing_example.png +0 -0
  21. {jararaca-0.3.18 → jararaca-0.3.19}/docs/expose-type.md +0 -0
  22. {jararaca-0.3.18 → jararaca-0.3.19}/docs/http-rpc.md +0 -0
  23. {jararaca-0.3.18 → jararaca-0.3.19}/docs/index.md +0 -0
  24. {jararaca-0.3.18 → jararaca-0.3.19}/docs/interceptors.md +0 -0
  25. {jararaca-0.3.18 → jararaca-0.3.19}/docs/messagebus.md +0 -0
  26. {jararaca-0.3.18 → jararaca-0.3.19}/docs/retry.md +0 -0
  27. {jararaca-0.3.18 → jararaca-0.3.19}/docs/scheduler.md +0 -0
  28. {jararaca-0.3.18 → jararaca-0.3.19}/docs/stylesheets/custom.css +0 -0
  29. {jararaca-0.3.18 → jararaca-0.3.19}/docs/websocket.md +0 -0
  30. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/__main__.py +0 -0
  31. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/broker_backend/__init__.py +0 -0
  32. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/broker_backend/mapper.py +0 -0
  33. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/broker_backend/redis_broker_backend.py +0 -0
  34. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/cli.py +0 -0
  35. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/common/__init__.py +0 -0
  36. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/core/__init__.py +0 -0
  37. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/core/providers.py +0 -0
  38. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/di.py +0 -0
  39. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/files/entity.py.mako +0 -0
  40. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/lifecycle.py +0 -0
  41. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/__init__.py +0 -0
  42. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/bus_message_controller.py +0 -0
  43. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/consumers/__init__.py +0 -0
  44. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/decorators.py +0 -0
  45. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/interceptors/__init__.py +0 -0
  46. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/interceptors/publisher_interceptor.py +0 -0
  47. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/message.py +0 -0
  48. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/messagebus/publisher.py +0 -0
  49. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/observability/interceptor.py +0 -0
  50. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/observability/providers/__init__.py +0 -0
  51. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/base.py +0 -0
  52. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/exports.py +0 -0
  53. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/interceptors/__init__.py +0 -0
  54. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/interceptors/aiosqa_interceptor.py +0 -0
  55. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/interceptors/constants.py +0 -0
  56. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/interceptors/decorators.py +0 -0
  57. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/session.py +0 -0
  58. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/sort_filter.py +0 -0
  59. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/persistence/utilities.py +0 -0
  60. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/__init__.py +0 -0
  61. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/decorators.py +0 -0
  62. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/hooks.py +0 -0
  63. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/http_microservice.py +0 -0
  64. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/__init__.py +0 -0
  65. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/base_types.py +0 -0
  66. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/context.py +0 -0
  67. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/decorators.py +0 -0
  68. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/redis.py +0 -0
  69. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/types.py +0 -0
  70. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/presentation/websocket/websocket_interceptor.py +0 -0
  71. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/py.typed +0 -0
  72. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/reflect/__init__.py +0 -0
  73. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/reflect/controller_inspect.py +0 -0
  74. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/reflect/metadata.py +0 -0
  75. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/__init__.py +0 -0
  76. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/http/__init__.py +0 -0
  77. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/http/backends/__init__.py +0 -0
  78. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/http/backends/httpx.py +0 -0
  79. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/http/backends/otel.py +0 -0
  80. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/http/decorators.py +0 -0
  81. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/rpc/http/httpx.py +0 -0
  82. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/scheduler/__init__.py +0 -0
  83. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/scheduler/beat_worker.py +0 -0
  84. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/scheduler/decorators.py +0 -0
  85. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/scheduler/types.py +0 -0
  86. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/tools/app_config/__init__.py +0 -0
  87. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/tools/app_config/decorators.py +0 -0
  88. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/tools/app_config/interceptor.py +0 -0
  89. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/tools/typescript/__init__.py +0 -0
  90. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/tools/typescript/decorators.py +0 -0
  91. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/tools/typescript/interface_parser.py +0 -0
  92. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/utils/__init__.py +0 -0
  93. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/utils/rabbitmq_utils.py +0 -0
  94. {jararaca-0.3.18 → jararaca-0.3.19}/src/jararaca/utils/retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.18
3
+ Version: 0.3.19
4
4
  Summary: A simple and fast API framework for Python
5
5
  Home-page: https://github.com/LuscasLeo/jararaca
6
6
  Author: Lucas S
@@ -18,11 +18,11 @@ Requires-Dist: croniter (>=3.0.3,<4.0.0)
18
18
  Requires-Dist: fastapi (>=0.113.0,<0.114.0)
19
19
  Requires-Dist: frozendict (>=2.4.6,<3.0.0)
20
20
  Requires-Dist: mako (>=1.3.5,<2.0.0)
21
- Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
22
- Requires-Dist: opentelemetry-distro (>=0.49b2,<0.50) ; extra == "opentelemetry"
21
+ Requires-Dist: opentelemetry-api (>=1.38.0,<2.0.0) ; extra == "opentelemetry"
22
+ Requires-Dist: opentelemetry-distro (>=0.59b0,<0.60) ; extra == "opentelemetry"
23
23
  Requires-Dist: opentelemetry-exporter-otlp (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
24
24
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
25
- Requires-Dist: opentelemetry-sdk (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
25
+ Requires-Dist: opentelemetry-sdk (>=1.38.0,<2.0.0) ; extra == "opentelemetry"
26
26
  Requires-Dist: redis (>=5.0.8,<6.0.0)
27
27
  Requires-Dist: sqlalchemy (>=2.0.34,<3.0.0)
28
28
  Requires-Dist: types-croniter (>=3.0.3.20240731,<4.0.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.18"
3
+ version = "0.3.19"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"
@@ -20,9 +20,9 @@ croniter = "^3.0.3"
20
20
  redis = "^5.0.8"
21
21
  websockets = "^13.0.1"
22
22
  opentelemetry-exporter-otlp-proto-http = { version = "^1.27.0", optional = true }
23
- opentelemetry-api = { version = "^1.27.0", optional = true }
24
- opentelemetry-sdk = { version = "^1.27.0", optional = true }
25
- opentelemetry-distro = { version = "^0.49b2", optional = true }
23
+ opentelemetry-api = "^1.38.0"
24
+ opentelemetry-sdk = "^1.38.0"
25
+ opentelemetry-distro = "^0.59b0"
26
26
  opentelemetry-exporter-otlp = { version = "^1.27.0", optional = true }
27
27
  types-croniter = "^3.0.3.20240731"
28
28
  types-redis = "^4.6.0.20240903"
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
17
17
  AppTransactionContext,
18
18
  use_app_type,
19
19
  )
20
+ from jararaca.observability.decorators import TracedClass, TracedFunc, traced_class
21
+ from jararaca.observability.hooks import spawn_trace
20
22
  from jararaca.observability.interceptor import ObservabilityInterceptor
21
23
  from jararaca.observability.providers.otel import OtelObservabilityProvider
22
24
  from jararaca.persistence.sort_filter import (
@@ -222,6 +224,10 @@ if TYPE_CHECKING:
222
224
  "HttpPut",
223
225
  "HttpDelete",
224
226
  "ObservabilityInterceptor",
227
+ "TracedFunc",
228
+ "TracedClass",
229
+ "traced_class",
230
+ "spawn_trace",
225
231
  "QueryInjector",
226
232
  "HttpMicroservice",
227
233
  "use_current_container",
@@ -392,6 +398,10 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
392
398
  "HttpPut": (__SPEC_PARENT__, "rpc.http.decorators", "Put"),
393
399
  "HttpDelete": (__SPEC_PARENT__, "rpc.http.decorators", "Delete"),
394
400
  "ObservabilityInterceptor": (__SPEC_PARENT__, "observability.interceptor", None),
401
+ "TracedFunc": (__SPEC_PARENT__, "observability.decorators", None),
402
+ "TracedClass": (__SPEC_PARENT__, "observability.decorators", None),
403
+ "traced_class": (__SPEC_PARENT__, "observability.decorators", None),
404
+ "spawn_trace": (__SPEC_PARENT__, "observability.hooks", None),
395
405
  "QueryInjector": (__SPEC_PARENT__, "persistence.utilities", None),
396
406
  "HttpMicroservice": (__SPEC_PARENT__, "presentation.http_microservice", None),
397
407
  "use_current_container": (__SPEC_PARENT__, "microservice", None),
@@ -33,7 +33,6 @@ class UnitOfWorkContextProvider:
33
33
  self.container = container
34
34
  self.container_interceptor = ContainerInterceptor(container)
35
35
 
36
- # TODO: Guarantee that the context is closed whenever an exception is raised
37
36
  # TODO: Guarantee a unit of work workflow for the whole request, including all the interceptors
38
37
 
39
38
  def factory_app_interceptors(self) -> Sequence[AppInterceptor]:
@@ -65,7 +64,36 @@ class UnitOfWorkContextProvider:
65
64
  for ctx in ctxs:
66
65
  await ctx.__aenter__()
67
66
 
68
- yield None
69
-
70
- for ctx in reversed(ctxs):
71
- await ctx.__aexit__(None, None, None)
67
+ exc_type = None
68
+ exc_value = None
69
+ exc_traceback = None
70
+
71
+ try:
72
+ yield None
73
+ except BaseException as e:
74
+ exc_type = type(e)
75
+ exc_value = e
76
+ exc_traceback = e.__traceback__
77
+ raise
78
+ finally:
79
+ # Exit interceptors in reverse order, propagating exception info
80
+ for ctx in reversed(ctxs):
81
+ try:
82
+ suppressed = await ctx.__aexit__(
83
+ exc_type, exc_value, exc_traceback
84
+ )
85
+ # If an interceptor returns True, it suppresses the exception
86
+ if suppressed and exc_type is not None:
87
+ exc_type = None
88
+ exc_value = None
89
+ exc_traceback = None
90
+ except BaseException as exit_exc:
91
+ # If an interceptor raises an exception during cleanup,
92
+ # replace the original exception with the new one
93
+ exc_type = type(exit_exc)
94
+ exc_value = exit_exc
95
+ exc_traceback = exit_exc.__traceback__
96
+
97
+ # Re-raise the exception if it wasn't suppressed
98
+ if exc_type is not None and exc_value is not None:
99
+ raise exc_value.with_traceback(exc_traceback)
@@ -0,0 +1,45 @@
1
+ import datetime
2
+ import decimal
3
+ import typing
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+ from typing import Any, Dict, Generator
7
+
8
+ FieldArray = list["FieldValue"]
9
+ """A data structure for holding an array of field values."""
10
+
11
+ FieldTable = typing.Dict[str, "FieldValue"]
12
+ FieldValue = (
13
+ bool
14
+ | bytes
15
+ | bytearray
16
+ | decimal.Decimal
17
+ | FieldArray
18
+ | FieldTable
19
+ | float
20
+ | int
21
+ | None
22
+ | str
23
+ | datetime.datetime
24
+ )
25
+
26
+ ImplicitHeaders = Dict[str, FieldValue]
27
+
28
+ implicit_headers_ctx = ContextVar[ImplicitHeaders | None](
29
+ "implicit_headers_ctx", default=None
30
+ )
31
+
32
+
33
+ def use_implicit_headers() -> ImplicitHeaders | None:
34
+ return implicit_headers_ctx.get()
35
+
36
+
37
+ @contextmanager
38
+ def provide_implicit_headers(
39
+ implicit_headers: ImplicitHeaders,
40
+ ) -> Generator[None, Any, None]:
41
+ token = implicit_headers_ctx.set(implicit_headers)
42
+ try:
43
+ yield
44
+ finally:
45
+ implicit_headers_ctx.reset(token)
@@ -9,6 +9,7 @@ from aio_pika.abc import AbstractConnection
9
9
  from pydantic import BaseModel
10
10
 
11
11
  from jararaca.broker_backend import MessageBrokerBackend
12
+ from jararaca.messagebus import implicit_headers
12
13
  from jararaca.messagebus.interceptors.publisher_interceptor import (
13
14
  MessageBusConnectionFactory,
14
15
  )
@@ -42,8 +43,12 @@ class AIOPikaMessagePublisher(MessagePublisher):
42
43
  logging.warning(f"Exchange {self.exchange_name} not found")
43
44
  return
44
45
  routing_key = f"{topic}."
46
+
47
+ implicit_headers_data = implicit_headers.use_implicit_headers()
45
48
  await exchange.publish(
46
- aio_pika.Message(body=message.model_dump_json().encode()),
49
+ aio_pika.Message(
50
+ body=message.model_dump_json().encode(), headers=implicit_headers_data
51
+ ),
47
52
  routing_key=routing_key,
48
53
  )
49
54
 
@@ -50,6 +50,7 @@ from jararaca.messagebus.decorators import (
50
50
  MessageHandlerData,
51
51
  ScheduleDispatchData,
52
52
  )
53
+ from jararaca.messagebus.implicit_headers import provide_implicit_headers
53
54
  from jararaca.messagebus.message import Message, MessageOf
54
55
  from jararaca.microservice import (
55
56
  AppTransactionContext,
@@ -1130,6 +1131,8 @@ class ScheduledMessageHandlerCallback:
1130
1131
  AppTransactionContext(
1131
1132
  controller_member_reflect=scheduled_action.controller_member,
1132
1133
  transaction_data=SchedulerTransactionData(
1134
+ task_name=scheduled_action.spec.name
1135
+ or scheduled_action.callable.__qualname__,
1133
1136
  scheduled_to=datetime.now(UTC),
1134
1137
  cron_expression=scheduled_action.spec.cron,
1135
1138
  triggered_at=datetime.now(UTC),
@@ -1469,7 +1472,9 @@ class MessageHandlerCallback:
1469
1472
  incoming_message_spec = MessageHandler.get_message_incoming(handler)
1470
1473
  assert incoming_message_spec is not None
1471
1474
 
1472
- with provide_shutdown_state(self.consumer.shutdown_state):
1475
+ with provide_implicit_headers(aio_pika_message.headers), provide_shutdown_state(
1476
+ self.consumer.shutdown_state
1477
+ ):
1473
1478
  async with self.consumer.uow_context_provider(
1474
1479
  AppTransactionContext(
1475
1480
  controller_member_reflect=handler_data.controller_member,
@@ -19,7 +19,7 @@ from typing import (
19
19
  runtime_checkable,
20
20
  )
21
21
 
22
- from fastapi import Request, WebSocket
22
+ from fastapi import Request, Response, WebSocket
23
23
 
24
24
  from jararaca.core.providers import ProviderSpec, T, Token
25
25
  from jararaca.messagebus import MessageOf
@@ -34,6 +34,7 @@ if TYPE_CHECKING:
34
34
 
35
35
  @dataclass
36
36
  class SchedulerTransactionData:
37
+ task_name: str
37
38
  triggered_at: datetime
38
39
  scheduled_to: datetime
39
40
  cron_expression: str
@@ -43,6 +44,7 @@ class SchedulerTransactionData:
43
44
  @dataclass
44
45
  class HttpTransactionData:
45
46
  request: Request
47
+ response: Response
46
48
  context_type: Literal["http"] = "http"
47
49
 
48
50
 
@@ -0,0 +1,213 @@
1
+ import inspect
2
+ from contextlib import contextmanager, suppress
3
+ from contextvars import ContextVar
4
+ from functools import wraps
5
+ from typing import (
6
+ Any,
7
+ AsyncContextManager,
8
+ Awaitable,
9
+ Callable,
10
+ ContextManager,
11
+ Generator,
12
+ Mapping,
13
+ Protocol,
14
+ Sequence,
15
+ TypeVar,
16
+ Union,
17
+ )
18
+
19
+ from jararaca.microservice import AppTransactionContext
20
+
21
+ F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
22
+
23
+ AttributeValue = Union[
24
+ str,
25
+ bool,
26
+ int,
27
+ float,
28
+ Sequence[str],
29
+ Sequence[bool],
30
+ Sequence[int],
31
+ Sequence[float],
32
+ ]
33
+
34
+ AttributeMap = Mapping[str, AttributeValue]
35
+
36
+
37
+ class TracingContextProvider(Protocol):
38
+
39
+ def __call__(
40
+ self, trace_name: str, context_attributes: AttributeMap | None
41
+ ) -> ContextManager[Any]: ...
42
+
43
+
44
+ class TracingContextProviderFactory(Protocol):
45
+
46
+ def root_setup(
47
+ self, app_context: AppTransactionContext
48
+ ) -> AsyncContextManager[None]: ...
49
+
50
+ def provide_provider(
51
+ self, app_context: AppTransactionContext
52
+ ) -> TracingContextProvider: ...
53
+
54
+
55
+ tracing_ctx_provider_ctxv = ContextVar[TracingContextProvider]("tracing_ctx_provider")
56
+
57
+
58
+ @contextmanager
59
+ def provide_tracing_ctx_provider(
60
+ ctx_provider: TracingContextProvider,
61
+ ) -> Generator[None, None, None]:
62
+
63
+ token = tracing_ctx_provider_ctxv.set(ctx_provider)
64
+ try:
65
+ yield
66
+ finally:
67
+ with suppress(ValueError):
68
+ tracing_ctx_provider_ctxv.reset(token)
69
+
70
+
71
+ def get_tracing_ctx_provider() -> TracingContextProvider | None:
72
+ return tracing_ctx_provider_ctxv.get(None)
73
+
74
+
75
+ def default_trace_mapper(*args: Any, **kwargs: Any) -> dict[str, str]:
76
+ return {
77
+ "args": str(args),
78
+ "kwargs": str(kwargs),
79
+ }
80
+
81
+
82
+ class TracedFunc:
83
+
84
+ def __init__(
85
+ self,
86
+ trace_name: str,
87
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
88
+ ):
89
+ self.trace_name = trace_name
90
+ self.trace_mapper = trace_mapper
91
+
92
+ def __call__(
93
+ self,
94
+ decorated: F,
95
+ ) -> F:
96
+
97
+ @wraps(decorated)
98
+ async def wrapper(
99
+ *args: Any,
100
+ **kwargs: Any,
101
+ ) -> Any:
102
+
103
+ if ctx_provider := get_tracing_ctx_provider():
104
+ with ctx_provider(
105
+ self.trace_name,
106
+ self.trace_mapper(*args, **kwargs),
107
+ ):
108
+ return await decorated(*args, **kwargs)
109
+
110
+ return await decorated(*args, **kwargs)
111
+
112
+ return wrapper # type: ignore[return-value]
113
+
114
+
115
+ C = TypeVar("C", bound=type)
116
+
117
+
118
+ class TracedClass:
119
+ """
120
+ Class decorator that automatically applies tracing to all async methods in a class.
121
+
122
+ Usage:
123
+ @TracedClass()
124
+ class MyService:
125
+ async def method1(self) -> str:
126
+ return "hello"
127
+
128
+ async def method2(self, x: int) -> int:
129
+ return x * 2
130
+
131
+ def sync_method(self) -> str: # Not traced
132
+ return "sync"
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ trace_name_prefix: str | None = None,
138
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
139
+ include_private: bool = False,
140
+ exclude_methods: set[str] | None = None,
141
+ ):
142
+ """
143
+ Initialize the TracedClass decorator.
144
+
145
+ Args:
146
+ trace_name_prefix: Prefix for trace names. If None, uses class name.
147
+ trace_mapper: Function to map method arguments to trace attributes.
148
+ include_private: Whether to trace private methods (starting with _).
149
+ exclude_methods: Set of method names to exclude from tracing.
150
+ """
151
+ self.trace_name_prefix = trace_name_prefix
152
+ self.trace_mapper = trace_mapper
153
+ self.include_private = include_private
154
+ self.exclude_methods = exclude_methods or set()
155
+
156
+ def __call__(self, cls: C) -> C:
157
+ """Apply tracing to all async methods in the class."""
158
+
159
+ # Use class name as prefix if not provided
160
+ trace_prefix = self.trace_name_prefix or cls.__name__
161
+
162
+ # Get all methods in the class
163
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
164
+ # Skip if method should be excluded
165
+ if name in self.exclude_methods:
166
+ continue
167
+
168
+ # Skip private methods unless explicitly included
169
+ if name.startswith("_") and not self.include_private:
170
+ continue
171
+
172
+ # Only trace async methods
173
+ if inspect.iscoroutinefunction(method):
174
+ trace_name = f"{trace_prefix}.{name}"
175
+ traced_method = TracedFunc(trace_name, self.trace_mapper)(method)
176
+ setattr(cls, name, traced_method)
177
+
178
+ return cls
179
+
180
+
181
+ def traced_class(
182
+ trace_name_prefix: str | None = None,
183
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
184
+ include_private: bool = False,
185
+ exclude_methods: set[str] | None = None,
186
+ ) -> Callable[[C], C]:
187
+ """
188
+ Functional interface for TracedClass decorator.
189
+
190
+ Usage:
191
+ @traced_class(trace_name_prefix="MyService")
192
+ class MyService:
193
+ async def method1(self) -> str:
194
+ return "hello"
195
+ """
196
+ return TracedClass(
197
+ trace_name_prefix=trace_name_prefix,
198
+ trace_mapper=trace_mapper,
199
+ include_private=include_private,
200
+ exclude_methods=exclude_methods,
201
+ )
202
+
203
+
204
+ __all__ = [
205
+ "TracingContextProvider",
206
+ "TracingContextProviderFactory",
207
+ "provide_tracing_ctx_provider",
208
+ "get_tracing_ctx_provider",
209
+ "default_trace_mapper",
210
+ "TracedFunc",
211
+ "TracedClass",
212
+ "traced_class",
213
+ ]
@@ -0,0 +1,20 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any, Generator
3
+
4
+ from jararaca.observability.decorators import AttributeMap, get_tracing_ctx_provider
5
+
6
+
7
+ @contextmanager
8
+ def spawn_trace(
9
+ name: str,
10
+ attributes: AttributeMap | None = None,
11
+ ) -> Generator[None, Any, None]:
12
+
13
+ if trace_context_provider := get_tracing_ctx_provider():
14
+ with trace_context_provider(trace_name=name, context_attributes=attributes):
15
+ yield
16
+ else:
17
+ yield
18
+
19
+
20
+ spawn_trace
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from contextlib import asynccontextmanager, contextmanager
3
- from typing import AsyncGenerator, Generator, Protocol
3
+ from typing import Any, AsyncGenerator, Generator, Protocol
4
4
 
5
5
  from opentelemetry import metrics, trace
6
6
  from opentelemetry._logs import set_logger_provider
@@ -23,11 +23,21 @@ from opentelemetry.sdk.trace import TracerProvider
23
23
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
24
24
  from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
25
25
 
26
- from jararaca.microservice import AppTransactionContext, Container, Microservice
26
+ from jararaca.messagebus.implicit_headers import (
27
+ ImplicitHeaders,
28
+ provide_implicit_headers,
29
+ use_implicit_headers,
30
+ )
31
+ from jararaca.microservice import (
32
+ AppTransactionContext,
33
+ Container,
34
+ Microservice,
35
+ use_app_transaction_context,
36
+ )
27
37
  from jararaca.observability.decorators import (
38
+ AttributeMap,
28
39
  TracingContextProvider,
29
40
  TracingContextProviderFactory,
30
- get_tracing_ctx_provider,
31
41
  )
32
42
  from jararaca.observability.interceptor import ObservabilityProvider
33
43
 
@@ -43,7 +53,7 @@ class OtelTracingContextProvider(TracingContextProvider):
43
53
  def __call__(
44
54
  self,
45
55
  trace_name: str,
46
- context_attributes: dict[str, str],
56
+ context_attributes: AttributeMap | None,
47
57
  ) -> Generator[None, None, None]:
48
58
 
49
59
  with tracer.start_as_current_span(trace_name, attributes=context_attributes):
@@ -63,7 +73,7 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
63
73
  ) -> AsyncGenerator[None, None]:
64
74
 
65
75
  title: str = "Unmapped App Context Execution"
66
- headers = {}
76
+ headers: dict[str, Any] = {}
67
77
  tx_data = app_tx_ctx.transaction_data
68
78
  if tx_data.context_type == "http":
69
79
 
@@ -72,6 +82,19 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
72
82
 
73
83
  elif tx_data.context_type == "message_bus":
74
84
  title = f"Message Bus {tx_data.topic}"
85
+ headers = use_implicit_headers() or {}
86
+
87
+ elif tx_data.context_type == "websocket":
88
+ headers = dict(tx_data.websocket.headers)
89
+ title = f"WebSocket {tx_data.websocket.url}"
90
+
91
+ elif tx_data.context_type == "scheduler":
92
+ title = f"Scheduler Task {tx_data.task_name}"
93
+ headers = {
94
+ "scheduled_to": tx_data.scheduled_to.isoformat(),
95
+ "cron_expression": tx_data.cron_expression,
96
+ "triggered_at": tx_data.triggered_at.isoformat(),
97
+ }
75
98
 
76
99
  carrier = {
77
100
  key: value
@@ -90,8 +113,23 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
90
113
 
91
114
  ctx2 = W3CBaggagePropagator().extract(b2, context=ctx)
92
115
 
93
- with tracer.start_as_current_span(name=title, context=ctx2):
94
- yield
116
+ with tracer.start_as_current_span(
117
+ name=title,
118
+ context=ctx2,
119
+ attributes={
120
+ "app.context_type": tx_data.context_type,
121
+ },
122
+ ) as root_span:
123
+ cx = root_span.get_span_context()
124
+ if app_tx_ctx.transaction_data.context_type == "http":
125
+ app_tx_ctx.transaction_data.response.headers["traceparent"] = hex(
126
+ cx.trace_id
127
+ )[2:].rjust(32, "0")
128
+ tracing_headers: ImplicitHeaders = {}
129
+ TraceContextTextMapPropagator().inject(tracing_headers)
130
+ W3CBaggagePropagator().inject(tracing_headers)
131
+ with provide_implicit_headers(tracing_headers):
132
+ yield
95
133
 
96
134
 
97
135
  class LoggerHandlerCallback(Protocol):
@@ -99,6 +137,21 @@ class LoggerHandlerCallback(Protocol):
99
137
  def __call__(self, logger_handler: logging.Handler) -> None: ...
100
138
 
101
139
 
140
+ class CustomLoggingHandler(LoggingHandler):
141
+
142
+ def _translate(self, record: logging.LogRecord) -> dict[str, Any]:
143
+ try:
144
+ ctx = use_app_transaction_context()
145
+ return {
146
+ **super()._translate(record),
147
+ "attributes": {
148
+ "context_type": ctx.transaction_data.context_type,
149
+ },
150
+ }
151
+ except LookupError:
152
+ return super()._translate(record)
153
+
154
+
102
155
  class OtelObservabilityProvider(ObservabilityProvider):
103
156
 
104
157
  def __init__(
@@ -143,11 +196,11 @@ class OtelObservabilityProvider(ObservabilityProvider):
143
196
  BatchLogRecordProcessor(self.logs_exporter)
144
197
  )
145
198
 
146
- logging_handler = LoggingHandler(
199
+ logging_handler = CustomLoggingHandler(
147
200
  level=logging.DEBUG, logger_provider=logger_provider
148
201
  )
149
202
 
150
- logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
203
+ # logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
151
204
 
152
205
  self.logging_handler_callback(logging_handler)
153
206
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import os
2
3
  import signal
3
4
  import threading
@@ -5,7 +6,7 @@ from contextlib import asynccontextmanager
5
6
  from signal import SIGINT, SIGTERM
6
7
  from typing import Any, AsyncGenerator
7
8
 
8
- from fastapi import Depends, FastAPI, Request, WebSocket
9
+ from fastapi import Depends, FastAPI, HTTPException, Request, Response, WebSocket
9
10
  from starlette.types import ASGIApp
10
11
 
11
12
  from jararaca.core.uow import UnitOfWorkContextProvider
@@ -23,6 +24,8 @@ from jararaca.presentation.decorators import RestController
23
24
  from jararaca.presentation.http_microservice import HttpMicroservice
24
25
  from jararaca.reflect.controller_inspect import ControllerMemberReflect
25
26
 
27
+ logger = logging.getLogger(__name__)
28
+
26
29
 
27
30
  class HttpAppLifecycle:
28
31
 
@@ -109,7 +112,7 @@ class HttpUowContextProviderDependency:
109
112
  self.shutdown_state.setup_signal_handlers()
110
113
 
111
114
  async def __call__(
112
- self, websocket: WebSocket = None, request: Request = None # type: ignore
115
+ self, websocket: WebSocket = None, request: Request = None, response: Response = None # type: ignore
113
116
  ) -> AsyncGenerator[None, None]:
114
117
  if request:
115
118
  endpoint = request.scope["endpoint"]
@@ -133,13 +136,34 @@ class HttpUowContextProviderDependency:
133
136
  AppTransactionContext(
134
137
  controller_member_reflect=member,
135
138
  transaction_data=(
136
- HttpTransactionData(request=request)
139
+ HttpTransactionData(request=request, response=response)
137
140
  if request
138
141
  else WebSocketTransactionData(websocket=websocket)
139
142
  ),
140
143
  )
141
144
  ):
142
- yield
145
+ try:
146
+ yield
147
+ except HTTPException:
148
+ raise
149
+ except Exception as e:
150
+ logger.exception("Unhandled exception in request handling.")
151
+ raise HTTPException(
152
+ status_code=500,
153
+ detail={
154
+ "message": "Internal server error occurred.",
155
+ "x-traceparentid": (
156
+ response.headers.get("traceparent")
157
+ if response
158
+ else None
159
+ ),
160
+ },
161
+ headers=(
162
+ {k: str(v) for k, v in response.headers.items()}
163
+ if response
164
+ else {}
165
+ ),
166
+ ) from e
143
167
 
144
168
 
145
169
  def create_http_server(
@@ -1,97 +0,0 @@
1
- from contextlib import contextmanager, suppress
2
- from contextvars import ContextVar
3
- from functools import wraps
4
- from typing import (
5
- Any,
6
- AsyncContextManager,
7
- Awaitable,
8
- Callable,
9
- ContextManager,
10
- Generator,
11
- ParamSpec,
12
- Protocol,
13
- TypeVar,
14
- )
15
-
16
- from jararaca.microservice import AppTransactionContext
17
-
18
- P = ParamSpec("P")
19
- R = TypeVar("R")
20
-
21
-
22
- class TracingContextProvider(Protocol):
23
-
24
- def __call__(
25
- self, trace_name: str, context_attributes: dict[str, str]
26
- ) -> ContextManager[Any]: ...
27
-
28
-
29
- class TracingContextProviderFactory(Protocol):
30
-
31
- def root_setup(
32
- self, app_context: AppTransactionContext
33
- ) -> AsyncContextManager[None]: ...
34
-
35
- def provide_provider(
36
- self, app_context: AppTransactionContext
37
- ) -> TracingContextProvider: ...
38
-
39
-
40
- tracing_ctx_provider_ctxv = ContextVar[TracingContextProvider]("tracing_ctx_provider")
41
-
42
-
43
- @contextmanager
44
- def provide_tracing_ctx_provider(
45
- ctx_provider: TracingContextProvider,
46
- ) -> Generator[None, None, None]:
47
-
48
- token = tracing_ctx_provider_ctxv.set(ctx_provider)
49
- try:
50
- yield
51
- finally:
52
- with suppress(ValueError):
53
- tracing_ctx_provider_ctxv.reset(token)
54
-
55
-
56
- def get_tracing_ctx_provider() -> TracingContextProvider | None:
57
- return tracing_ctx_provider_ctxv.get(None)
58
-
59
-
60
- def default_trace_mapper(*args: Any, **kwargs: Any) -> dict[str, str]:
61
- return {
62
- "args": str(args),
63
- "kwargs": str(kwargs),
64
- }
65
-
66
-
67
- class TracedFunc:
68
-
69
- def __init__(
70
- self,
71
- trace_name: str,
72
- trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
73
- ):
74
- self.trace_name = trace_name
75
- self.trace_mapper = trace_mapper
76
-
77
- def __call__(
78
- self,
79
- decorated: Callable[P, Awaitable[R]],
80
- ) -> Callable[P, Awaitable[R]]:
81
-
82
- @wraps(decorated)
83
- async def wrapper(
84
- *args: P.args,
85
- **kwargs: P.kwargs,
86
- ) -> R:
87
-
88
- if ctx_provider := get_tracing_ctx_provider():
89
- with ctx_provider(
90
- self.trace_name,
91
- self.trace_mapper(**kwargs),
92
- ):
93
- return await decorated(*args, **kwargs)
94
-
95
- return await decorated(*args, **kwargs)
96
-
97
- return wrapper
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes