fastapi-factory-utilities 0.2.0__py3-none-any.whl → 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fastapi-factory-utilities might be problematic. Click here for more details.

Files changed (72) hide show
  1. fastapi_factory_utilities/core/api/__init__.py +1 -1
  2. fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
  3. fastapi_factory_utilities/core/app/__init__.py +12 -3
  4. fastapi_factory_utilities/core/app/application.py +24 -26
  5. fastapi_factory_utilities/core/app/builder.py +23 -37
  6. fastapi_factory_utilities/core/app/config.py +22 -1
  7. fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
  8. fastapi_factory_utilities/core/exceptions.py +58 -22
  9. fastapi_factory_utilities/core/plugins/__init__.py +2 -31
  10. fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
  11. fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
  12. fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
  13. fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
  14. fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
  15. fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
  16. fastapi_factory_utilities/core/plugins/aiopika/exchange.py +70 -0
  17. fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
  18. fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
  19. fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
  20. fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
  21. fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
  22. fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
  23. fastapi_factory_utilities/core/plugins/aiopika/queue.py +86 -0
  24. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +25 -153
  25. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +59 -31
  26. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
  27. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +2 -1
  28. fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
  29. fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
  30. fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +112 -3
  31. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -115
  32. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +65 -14
  33. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +13 -0
  34. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
  35. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
  36. fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +29 -0
  37. fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
  38. fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
  39. fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
  40. fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
  41. fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
  42. fastapi_factory_utilities/core/protocols.py +1 -54
  43. fastapi_factory_utilities/core/security/jwt.py +159 -0
  44. fastapi_factory_utilities/core/security/kratos.py +98 -0
  45. fastapi_factory_utilities/core/services/hydra/__init__.py +13 -0
  46. fastapi_factory_utilities/core/services/hydra/exceptions.py +15 -0
  47. fastapi_factory_utilities/core/services/hydra/objects.py +26 -0
  48. fastapi_factory_utilities/core/services/hydra/services.py +122 -0
  49. fastapi_factory_utilities/core/services/kratos/__init__.py +13 -0
  50. fastapi_factory_utilities/core/services/kratos/enums.py +11 -0
  51. fastapi_factory_utilities/core/services/kratos/exceptions.py +15 -0
  52. fastapi_factory_utilities/core/services/kratos/objects.py +43 -0
  53. fastapi_factory_utilities/core/services/kratos/services.py +86 -0
  54. fastapi_factory_utilities/core/services/status/__init__.py +2 -2
  55. fastapi_factory_utilities/core/utils/status.py +2 -1
  56. fastapi_factory_utilities/core/utils/uvicorn.py +36 -0
  57. fastapi_factory_utilities/core/utils/yaml_reader.py +2 -2
  58. fastapi_factory_utilities/example/app.py +15 -5
  59. fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
  60. fastapi_factory_utilities/example/models/books/__init__.py +1 -1
  61. fastapi_factory_utilities/py.typed +0 -0
  62. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/METADATA +23 -14
  63. fastapi_factory_utilities-0.7.1.dist-info/RECORD +101 -0
  64. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/WHEEL +1 -1
  65. fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
  66. fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
  67. fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
  68. fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
  69. fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
  70. fastapi_factory_utilities-0.2.0.dist-info/RECORD +0 -70
  71. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/entry_points.txt +0 -0
  72. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info/licenses}/LICENSE +0 -0
@@ -1,124 +1,17 @@
1
1
  """OpenTelemetry Plugin Module."""
2
2
 
3
- import asyncio
4
- from typing import cast
5
-
6
- from opentelemetry.instrumentation.fastapi import ( # pyright: ignore[reportMissingTypeStubs]
7
- FastAPIInstrumentor,
8
- )
9
- from opentelemetry.sdk.metrics import MeterProvider
10
- from opentelemetry.sdk.trace import TracerProvider
11
- from structlog.stdlib import BoundLogger, get_logger
12
-
13
- from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
14
-
15
- from .builder import OpenTelemetryPluginBuilder
16
- from .configs import OpenTelemetryConfig
3
+ from .configs import OpenTelemetryConfig, OpenTelemetryMeterConfig, OpenTelemetryTracerConfig
17
4
  from .exceptions import OpenTelemetryPluginBaseException, OpenTelemetryPluginConfigError
5
+ from .plugins import OpenTelemetryPlugin, depends_meter_provider, depends_otel_config, depends_tracer_provider
18
6
 
19
7
  __all__: list[str] = [
20
8
  "OpenTelemetryConfig",
9
+ "OpenTelemetryMeterConfig",
10
+ "OpenTelemetryPlugin",
21
11
  "OpenTelemetryPluginBaseException",
22
12
  "OpenTelemetryPluginConfigError",
23
- "OpenTelemetryPluginBuilder",
13
+ "OpenTelemetryTracerConfig",
14
+ "depends_meter_provider",
15
+ "depends_otel_config",
16
+ "depends_tracer_provider",
24
17
  ]
25
-
26
- _logger: BoundLogger = get_logger()
27
-
28
-
29
- def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
30
- """Check the pre-conditions for the OpenTelemetry plugin.
31
-
32
- Args:
33
- application (BaseApplicationProtocol): The application.
34
-
35
- Returns:
36
- bool: True if the pre-conditions are met, False otherwise.
37
- """
38
- del application
39
- return True
40
-
41
-
42
- def on_load(
43
- application: ApplicationAbstractProtocol,
44
- ) -> None:
45
- """Actions to perform on load for the OpenTelemetry plugin.
46
-
47
- Args:
48
- application (BaseApplicationProtocol): The application.
49
- """
50
- # Build the OpenTelemetry Resources, TracerProvider and MeterProvider
51
- try:
52
- otel_builder: OpenTelemetryPluginBuilder = OpenTelemetryPluginBuilder(application=application).build_all()
53
- except OpenTelemetryPluginBaseException as exception:
54
- _logger.error(f"OpenTelemetry plugin failed to start. {exception}")
55
- return
56
- # Configuration is never None at this point (checked in the builder and raises an exception)
57
- otel_config: OpenTelemetryConfig = cast(OpenTelemetryConfig, otel_builder.config)
58
- # Save as state in the FastAPI application
59
- application.get_asgi_app().state.tracer_provider = otel_builder.tracer_provider
60
- application.get_asgi_app().state.meter_provider = otel_builder.meter_provider
61
- application.get_asgi_app().state.otel_config = otel_config
62
- # Instrument the FastAPI application
63
- FastAPIInstrumentor.instrument_app( # pyright: ignore[reportUnknownMemberType]
64
- app=application.get_asgi_app(),
65
- tracer_provider=otel_builder.tracer_provider,
66
- meter_provider=otel_builder.meter_provider,
67
- excluded_urls=otel_config.excluded_urls,
68
- )
69
-
70
- _logger.debug(f"OpenTelemetry plugin loaded. {otel_config.activate=}")
71
-
72
-
73
- async def on_startup(
74
- application: ApplicationAbstractProtocol,
75
- ) -> None:
76
- """Actions to perform on startup for the OpenTelemetry plugin.
77
-
78
- Args:
79
- application (BaseApplicationProtocol): The application.
80
-
81
- Returns:
82
- None
83
- """
84
- del application
85
- _logger.debug("OpenTelemetry plugin started.")
86
-
87
-
88
- async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
89
- """Actions to perform on shutdown for the OpenTelemetry plugin.
90
-
91
- Args:
92
- application (BaseApplicationProtocol): The application.
93
-
94
- Returns:
95
- None
96
- """
97
- tracer_provider: TracerProvider = application.get_asgi_app().state.tracer_provider
98
- meter_provider: MeterProvider = application.get_asgi_app().state.meter_provider
99
- otel_config: OpenTelemetryConfig = application.get_asgi_app().state.otel_config
100
-
101
- seconds_to_ms_multiplier: int = 1000
102
-
103
- async def close_tracer_provider() -> None:
104
- """Close the tracer provider."""
105
- tracer_provider.force_flush(timeout_millis=otel_config.closing_timeout * seconds_to_ms_multiplier)
106
- # No Delay for the shutdown of the tracer provider
107
- tracer_provider.shutdown()
108
-
109
- async def close_meter_provider() -> None:
110
- """Close the meter provider.
111
-
112
- Split the timeout in half for the flush and shutdown.
113
- """
114
- meter_provider.force_flush(timeout_millis=int(otel_config.closing_timeout / 2) * seconds_to_ms_multiplier)
115
- meter_provider.shutdown(timeout_millis=int(otel_config.closing_timeout / 2) * seconds_to_ms_multiplier)
116
-
117
- _logger.debug("OpenTelemetry plugin stop requested. Flushing and closing...")
118
-
119
- await asyncio.gather(
120
- close_tracer_provider(),
121
- close_meter_provider(),
122
- )
123
-
124
- _logger.debug("OpenTelemetry plugin closed.")
@@ -1,9 +1,20 @@
1
1
  """Provides a factory function to build a objets for OpenTelemetry."""
2
2
 
3
3
  from typing import Any, Self
4
+ from urllib.parse import ParseResult, urlparse
4
5
 
5
- from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
6
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
6
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
7
+ OTLPMetricExporter as OTLPMetricExporterGRPC,
8
+ )
9
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
10
+ OTLPSpanExporter as OTLPSpanExporterGRPC,
11
+ )
12
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
13
+ OTLPMetricExporter as OTLPMetricExporterHTTP,
14
+ )
15
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
16
+ OTLPSpanExporter as OTLPSpanExporterHTTP,
17
+ )
7
18
  from opentelemetry.metrics import set_meter_provider
8
19
  from opentelemetry.propagate import set_global_textmap
9
20
  from opentelemetry.propagators.b3 import B3MultiFormat
@@ -31,24 +42,30 @@ from fastapi_factory_utilities.core.utils.yaml_reader import (
31
42
  YamlFileReader,
32
43
  )
33
44
 
34
- from .configs import OpenTelemetryConfig
45
+ from .configs import OpenTelemetryConfig, ProtocolEnum
35
46
  from .exceptions import OpenTelemetryPluginConfigError
36
47
 
48
+ GRPC_PORT: int = 4317
49
+ HTTP_PORT: int = 4318
50
+
37
51
 
38
52
  class OpenTelemetryPluginBuilder:
39
53
  """Configure the injection bindings for OpenTelemetryPlugin."""
40
54
 
41
- def __init__(self, application: ApplicationAbstractProtocol) -> None:
55
+ def __init__(self, application: ApplicationAbstractProtocol, settings: OpenTelemetryConfig | None = None) -> None:
42
56
  """Instantiate the OpenTelemetryPluginFactory.
43
57
 
44
58
  Args:
45
59
  application (BaseApplicationProtocol): The application object.
60
+ settings (OpenTelemetryConfig | None): The OpenTelemetry configuration object.
46
61
  """
47
62
  self._application: ApplicationAbstractProtocol = application
48
63
  self._resource: Resource | None = None
49
- self._config: OpenTelemetryConfig | None = None
64
+ self._config: OpenTelemetryConfig | None = settings
50
65
  self._meter_provider: MeterProvider | None = None
51
66
  self._tracer_provider: TracerProvider | None = None
67
+ self._trace_exporter: OTLPSpanExporterGRPC | OTLPSpanExporterHTTP | None = None
68
+ self._metric_exporter: OTLPMetricExporterGRPC | OTLPMetricExporterHTTP | None = None
52
69
 
53
70
  @property
54
71
  def resource(self) -> Resource | None:
@@ -161,11 +178,27 @@ class OpenTelemetryPluginBuilder:
161
178
 
162
179
  # TODO: Extract to a dedicated method for the exporter and period reader setup
163
180
 
181
+ url_parsed: ParseResult = urlparse(self._config.endpoint.unicode_string())
182
+
183
+ exporter: OTLPMetricExporterGRPC | OTLPMetricExporterHTTP
164
184
  # Setup the Exporter
165
- exporter = OTLPMetricExporter(
166
- endpoint=f"{self._config.endpoint.unicode_string()}v1/metrics",
167
- timeout=self._config.timeout,
168
- )
185
+ if url_parsed.port == GRPC_PORT or self._config.protocol == ProtocolEnum.OTLP_GRPC:
186
+ exporter = OTLPMetricExporterGRPC(
187
+ endpoint=f"{self._config.endpoint.unicode_string()}",
188
+ timeout=self._config.timeout,
189
+ insecure=True if str(self._config.endpoint).startswith("http") else False,
190
+ )
191
+ elif url_parsed.port == HTTP_PORT or self._config.protocol == ProtocolEnum.OTLP_HTTP:
192
+ exporter = OTLPMetricExporterHTTP(
193
+ endpoint=f"{self._config.endpoint.unicode_string()}/v1/metrics",
194
+ timeout=self._config.timeout,
195
+ )
196
+ else:
197
+ raise OpenTelemetryPluginConfigError(
198
+ "The endpoint port is not supported. Use 4317 for gRPC or 4318 for HTTP or set the protocol."
199
+ )
200
+
201
+ self._metric_exporter = exporter
169
202
 
170
203
  # Setup the Metric Reader
171
204
  meter_config: OpenTelemetryMeterConfig = self._config.meter_config
@@ -213,11 +246,28 @@ class OpenTelemetryPluginBuilder:
213
246
  if self._config.tracer_config is None:
214
247
  raise OpenTelemetryPluginConfigError("The tracer configuration is missing.")
215
248
 
249
+ exporter: OTLPSpanExporterGRPC | OTLPSpanExporterHTTP
250
+ url_parsed: ParseResult = urlparse(self._config.endpoint.unicode_string())
216
251
  # Setup the Exporter
217
- exporter = OTLPSpanExporter(
218
- endpoint=f"{self._config.endpoint.unicode_string()}v1/traces",
219
- timeout=self._config.timeout,
220
- )
252
+ if url_parsed.port == GRPC_PORT or self._config.protocol == ProtocolEnum.OTLP_GRPC:
253
+ insecure: bool = False if str(self._config.endpoint).startswith("https") else True
254
+ endpoint: str = f"{self._config.endpoint.unicode_string()}"
255
+ exporter = OTLPSpanExporterGRPC(
256
+ endpoint=endpoint,
257
+ # timeout=self._config.timeout,
258
+ insecure=insecure,
259
+ )
260
+ elif url_parsed.port == HTTP_PORT or self._config.protocol == ProtocolEnum.OTLP_HTTP:
261
+ exporter = OTLPSpanExporterHTTP(
262
+ endpoint=f"{self._config.endpoint.unicode_string()}/v1/traces",
263
+ # timeout=self._config.timeout,
264
+ )
265
+ else:
266
+ raise OpenTelemetryPluginConfigError(
267
+ "The endpoint port is not supported. Use 4317 for gRPC or 4318 for HTTP or set the protocol."
268
+ )
269
+
270
+ self._trace_exporter = exporter
221
271
 
222
272
  # Setup the Span Processor
223
273
  tracer_config: OpenTelemetryTracerConfig = self._config.tracer_config
@@ -259,7 +309,8 @@ class OpenTelemetryPluginBuilder:
259
309
  Self: The OpenTelemetryPluginFactory object.
260
310
  """
261
311
  self.build_resource()
262
- self.build_config()
312
+ if self._config is None:
313
+ self.build_config()
263
314
  self.build_meter_provider()
264
315
  self.build_tracer_provider()
265
316
 
@@ -1,11 +1,19 @@
1
1
  """Provides the configuration model for the OpenTelemetry plugin."""
2
2
 
3
+ from enum import StrEnum
3
4
  from typing import Annotated
4
5
 
5
6
  from pydantic import BaseModel, ConfigDict, Field, UrlConstraints
6
7
  from pydantic_core import Url
7
8
 
8
9
 
10
+ class ProtocolEnum(StrEnum):
11
+ """Defines the protocol enum for OpenTelemetry."""
12
+
13
+ OTLP_GRPC = "otlp_grpc"
14
+ OTLP_HTTP = "otlp_http"
15
+
16
+
9
17
  class OpenTelemetryMeterConfig(BaseModel):
10
18
  """Provides the configuration model for the OpenTelemetry meter as sub-model."""
11
19
 
@@ -77,6 +85,11 @@ class OpenTelemetryConfig(BaseModel):
77
85
  description="The collector endpoint.",
78
86
  )
79
87
 
88
+ protocol: ProtocolEnum | None = Field(
89
+ default=None,
90
+ description="The protocol to use for the collector.",
91
+ )
92
+
80
93
  timeout: int = Field(
81
94
  default=TEN_SECONDS_IN_SECONDS,
82
95
  description="The timeout in seconds for the collector.",
@@ -0,0 +1,85 @@
1
+ """Instruments for the OpenTelemetry plugin."""
2
+
3
+ # pyright: reportMissingTypeStubs=false
4
+
5
+ from collections.abc import Callable
6
+ from importlib.util import find_spec
7
+ from typing import Any
8
+
9
+ from opentelemetry.sdk.metrics import MeterProvider
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+
12
+ from fastapi_factory_utilities.core.plugins.opentelemetry_plugin.configs import OpenTelemetryConfig
13
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
14
+
15
+
16
+ def instrument_fastapi(
17
+ application: ApplicationAbstractProtocol,
18
+ config: OpenTelemetryConfig,
19
+ meter_provider: MeterProvider,
20
+ tracer_provider: TracerProvider,
21
+ ) -> None:
22
+ """Instrument the FastAPI application."""
23
+ if find_spec(name="fastapi") and find_spec(name="opentelemetry.instrumentation.fastapi"):
24
+ from opentelemetry.instrumentation.fastapi import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
25
+ FastAPIInstrumentor,
26
+ )
27
+
28
+ excluded_urls_str: str | None = None if len(config.excluded_urls) == 0 else ",".join(config.excluded_urls)
29
+ FastAPIInstrumentor.instrument_app( # pyright: ignore[reportUnknownMemberType]
30
+ app=application.get_asgi_app(),
31
+ tracer_provider=tracer_provider,
32
+ meter_provider=meter_provider,
33
+ excluded_urls=excluded_urls_str,
34
+ )
35
+
36
+
37
+ def instrument_aiohttp(
38
+ application: ApplicationAbstractProtocol, # pylint: disable=unused-argument
39
+ config: OpenTelemetryConfig, # pylint: disable=unused-argument
40
+ meter_provider: MeterProvider,
41
+ tracer_provider: TracerProvider,
42
+ ) -> None:
43
+ """Instrument the Aiohttp application.
44
+
45
+ Args:
46
+ application (ApplicationAbstractProtocol): The application.
47
+ config (OpenTelemetryConfig): The configuration.
48
+ meter_provider (MeterProvider): The meter provider.
49
+ tracer_provider (TracerProvider): The tracer provider.
50
+
51
+ Returns:
52
+ None
53
+ """
54
+ if find_spec(name="aiohttp") and find_spec(name="opentelemetry.instrumentation.aiohttp_client"):
55
+ from opentelemetry.instrumentation.aiohttp_client import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
56
+ AioHttpClientInstrumentor,
57
+ )
58
+
59
+ AioHttpClientInstrumentor().instrument( # pyright: ignore[reportUnknownMemberType]
60
+ tracer_provider=tracer_provider,
61
+ meter_provider=meter_provider,
62
+ )
63
+
64
+
65
+ def instrument_aio_pika(
66
+ application: ApplicationAbstractProtocol, # pylint: disable=unused-argument
67
+ config: OpenTelemetryConfig, # pylint: disable=unused-argument
68
+ meter_provider: MeterProvider,
69
+ tracer_provider: TracerProvider,
70
+ ) -> None:
71
+ """Instrument the AioPika application."""
72
+ if find_spec(name="aio_pika") and find_spec(name="opentelemetry.instrumentation.aio_pika"):
73
+ from opentelemetry.instrumentation.aio_pika import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
74
+ AioPikaInstrumentor,
75
+ )
76
+
77
+ AioPikaInstrumentor().instrument( # pyright: ignore[reportUnknownMemberType]
78
+ tracer_provider=tracer_provider,
79
+ meter_provider=meter_provider,
80
+ )
81
+
82
+
83
+ INSTRUMENTS: list[Callable[..., Any]] = [instrument_fastapi, instrument_aiohttp, instrument_aio_pika]
84
+
85
+ __all__: list[str] = ["INSTRUMENTS"]
@@ -0,0 +1,137 @@
1
+ """Provides the OpenTelemetry plugin."""
2
+
3
+ import asyncio
4
+ from typing import Self, cast
5
+
6
+ from fastapi import Request
7
+ from opentelemetry.sdk.metrics import MeterProvider
8
+ from opentelemetry.sdk.trace import TracerProvider
9
+ from structlog.stdlib import BoundLogger, get_logger
10
+
11
+ from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
12
+
13
+ from .builder import OpenTelemetryPluginBuilder
14
+ from .configs import OpenTelemetryConfig
15
+ from .exceptions import OpenTelemetryPluginBaseException, OpenTelemetryPluginConfigError
16
+ from .instruments import INSTRUMENTS
17
+
18
+ __all__: list[str] = [
19
+ "OpenTelemetryConfig",
20
+ "OpenTelemetryPluginBaseException",
21
+ "OpenTelemetryPluginBuilder",
22
+ "OpenTelemetryPluginConfigError",
23
+ ]
24
+
25
+ _logger: BoundLogger = get_logger()
26
+
27
+
28
+ class OpenTelemetryPlugin(PluginAbstract):
29
+ """OpenTelemetry plugin."""
30
+
31
+ SECONDS_TO_MS_MULTIPLIER: int = 1000
32
+
33
+ def __init__(self) -> None:
34
+ """Initialize the OpenTelemetry plugin."""
35
+ super().__init__()
36
+ self._otel_config: OpenTelemetryConfig | None = None
37
+ self._tracer_provider: TracerProvider | None = None
38
+ self._meter_provider: MeterProvider | None = None
39
+
40
+ def _build(self) -> Self:
41
+ """Build the OpenTelemetry plugin."""
42
+ assert self._application is not None
43
+ # Build the OpenTelemetry Resources, TracerProvider and MeterProvider
44
+ try:
45
+ otel_builder: OpenTelemetryPluginBuilder = OpenTelemetryPluginBuilder(
46
+ application=self._application
47
+ ).build_all()
48
+ except OpenTelemetryPluginBaseException as exception:
49
+ _logger.error(f"OpenTelemetry plugin failed to start. {exception}")
50
+ raise
51
+ # Configuration is never None at this point (checked in the builder and raises an exception)
52
+ self._otel_config = cast(OpenTelemetryConfig, otel_builder.config)
53
+ self._tracer_provider = otel_builder.tracer_provider
54
+ self._meter_provider = otel_builder.meter_provider
55
+ return self
56
+
57
+ def _instrument(self) -> None:
58
+ """Instrument the FastAPI application."""
59
+ assert self._application is not None
60
+ assert self._tracer_provider is not None
61
+ assert self._meter_provider is not None
62
+ assert self._otel_config is not None
63
+
64
+ for instrument in INSTRUMENTS:
65
+ instrument(self._application, self._otel_config, self._meter_provider, self._tracer_provider)
66
+
67
+ def on_load(self) -> None:
68
+ """On load."""
69
+ assert self._application is not None
70
+ # Build the OpenTelemetry Resources, TracerProvider and MeterProvider
71
+ self._build()
72
+ assert self._tracer_provider is not None
73
+ assert self._meter_provider is not None
74
+ assert self._otel_config is not None
75
+ self._add_to_state(key="tracer_provider", value=self._tracer_provider)
76
+ self._add_to_state(key="meter_provider", value=self._meter_provider)
77
+ self._add_to_state(key="otel_config", value=self._otel_config)
78
+ # Instrument the FastAPI application and AioHttpClient, ...
79
+ self._instrument()
80
+ _logger.debug(f"OpenTelemetry plugin loaded. {self._otel_config.activate=}")
81
+
82
+ async def on_startup(self) -> None:
83
+ """On startup."""
84
+ pass
85
+
86
+ async def close_tracer_provider(self) -> None:
87
+ """Close the tracer provider."""
88
+ assert self._tracer_provider is not None
89
+ assert self._otel_config is not None
90
+ self._tracer_provider.force_flush(
91
+ timeout_millis=self._otel_config.closing_timeout * self.SECONDS_TO_MS_MULTIPLIER
92
+ )
93
+ # No Delay for the shutdown of the tracer provider
94
+ try:
95
+ self._tracer_provider.shutdown()
96
+ except Exception as exception: # pylint: disable=broad-exception-caught
97
+ _logger.error("OpenTelemetry plugin failed to close the tracer provider.", error=exception)
98
+
99
+ async def close_meter_provider(self) -> None:
100
+ """Close the meter provider."""
101
+ assert self._meter_provider is not None
102
+ assert self._otel_config is not None
103
+ self._meter_provider.force_flush(
104
+ timeout_millis=self._otel_config.closing_timeout * self.SECONDS_TO_MS_MULTIPLIER
105
+ )
106
+ try:
107
+ self._meter_provider.shutdown(
108
+ timeout_millis=self._otel_config.closing_timeout * self.SECONDS_TO_MS_MULTIPLIER
109
+ )
110
+ except Exception as exception: # pylint: disable=broad-exception-caught
111
+ _logger.error("OpenTelemetry plugin failed to close the meter provider.", error=exception)
112
+
113
+ async def on_shutdown(self) -> None:
114
+ """On shutdown."""
115
+ _logger.debug("OpenTelemetry plugin stop requested. Flushing and closing...")
116
+
117
+ await asyncio.gather(
118
+ self.close_tracer_provider(),
119
+ self.close_meter_provider(),
120
+ )
121
+
122
+ _logger.debug("OpenTelemetry plugin closed.")
123
+
124
+
125
+ def depends_tracer_provider(request: Request) -> TracerProvider:
126
+ """Get the tracer provider."""
127
+ return request.app.state.tracer_provider
128
+
129
+
130
+ def depends_meter_provider(request: Request) -> MeterProvider:
131
+ """Get the meter provider."""
132
+ return request.app.state.meter_provider
133
+
134
+
135
+ def depends_otel_config(request: Request) -> OpenTelemetryConfig:
136
+ """Get the OpenTelemetry config."""
137
+ return request.app.state.otel_config
@@ -0,0 +1,29 @@
1
+ """Taskiq Plugin Module."""
2
+
3
+ from importlib.util import find_spec
4
+
5
+ from .depends import depends_scheduler_component
6
+ from .exceptions import TaskiqPluginBaseError
7
+ from .plugin import TaskiqPlugin
8
+ from .schedulers import SchedulerComponent
9
+
10
+ __all__: list[str] = [ # pylint: disable=invalid-name
11
+ "SchedulerComponent",
12
+ "TaskiqPlugin",
13
+ "TaskiqPluginBaseError",
14
+ "depends_scheduler_component",
15
+ ]
16
+
17
+ if find_spec("beanie") is not None:
18
+ from .depends import depends_odm_database
19
+
20
+ __all__ += [ # pylint: disable=invalid-name
21
+ "depends_odm_database",
22
+ ]
23
+
24
+ if find_spec("aio_pika") is not None:
25
+ from .depends import depends_aiopika_robust_connection
26
+
27
+ __all__ += [ # pylint: disable=invalid-name
28
+ "depends_aiopika_robust_connection",
29
+ ]
@@ -0,0 +1,12 @@
1
+ """Provides the configurations for the Taskiq plugin."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class RedisCredentialsConfig(BaseModel):
9
+ """Redis credentials config."""
10
+
11
+ model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
12
+ url: str
@@ -0,0 +1,51 @@
1
+ """Provides the dependencies for the Taskiq plugin."""
2
+
3
+ from importlib.util import find_spec
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from fastapi import Request
7
+ from taskiq import TaskiqDepends
8
+
9
+ if TYPE_CHECKING:
10
+ from .schedulers import SchedulerComponent
11
+
12
+ DEPENDS_SCHEDULER_COMPONENT_KEY: str = "scheduler_component"
13
+
14
+
15
+ def depends_scheduler_component(
16
+ request: Request = TaskiqDepends(),
17
+ ) -> "SchedulerComponent":
18
+ """Dependency injection for the scheduler component."""
19
+ return getattr(request.app.state, DEPENDS_SCHEDULER_COMPONENT_KEY)
20
+
21
+
22
+ if find_spec("beanie") is not None:
23
+ from motor.motor_asyncio import AsyncIOMotorDatabase
24
+
25
+ def depends_odm_database(request: Request = TaskiqDepends()) -> AsyncIOMotorDatabase[Any]:
26
+ """Acquire the ODM database from the request.
27
+
28
+ Args:
29
+ request (Request): The request.
30
+
31
+ Returns:
32
+ AsyncIOMotorClient: The ODM database.
33
+ """
34
+ return request.app.state.odm_database
35
+
36
+
37
+ if find_spec("aio_pika") is not None:
38
+ from aio_pika.abc import AbstractRobustConnection
39
+
40
+ from fastapi_factory_utilities.core.plugins.aiopika.depends import DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY
41
+
42
+ def depends_aiopika_robust_connection(request: Request = TaskiqDepends()) -> AbstractRobustConnection:
43
+ """Acquire the Aiopika robust connection from the request.
44
+
45
+ Args:
46
+ request (Request): The request.
47
+
48
+ Returns:
49
+ AbstractRobustConnection: The Aiopika robust connection.
50
+ """
51
+ return getattr(request.app.state, DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY)
@@ -0,0 +1,13 @@
1
+ """Provides the exceptions for the Taskiq plugin."""
2
+
3
+ from typing import Any
4
+
5
+ from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
6
+
7
+
8
+ class TaskiqPluginBaseError(FastAPIFactoryUtilitiesError):
9
+ """Base class for all exceptions raised by the Taskiq plugin."""
10
+
11
+ def __init__(self, message: str, **kwargs: Any) -> None:
12
+ """Initialize the Taskiq plugin base exception."""
13
+ super().__init__(message, **kwargs)
@@ -0,0 +1,41 @@
1
+ """Provides the Taskiq plugin."""
2
+
3
+ from collections.abc import Callable
4
+
5
+ from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
6
+
7
+ from .configs import RedisCredentialsConfig
8
+ from .depends import DEPENDS_SCHEDULER_COMPONENT_KEY
9
+ from .schedulers import SchedulerComponent
10
+
11
+
12
+ class TaskiqPlugin(PluginAbstract):
13
+ """Taskiq plugin."""
14
+
15
+ def __init__(
16
+ self, redis_credentials_config: RedisCredentialsConfig, register_hook: Callable[[SchedulerComponent], None]
17
+ ) -> None:
18
+ """Initialize the Taskiq plugin."""
19
+ super().__init__()
20
+ self._redis_credentials_config: RedisCredentialsConfig = redis_credentials_config
21
+ self._register_hook: Callable[[SchedulerComponent], None] = register_hook
22
+ self._scheduler_component: SchedulerComponent = SchedulerComponent()
23
+
24
+ def on_load(self) -> None:
25
+ """On load."""
26
+ assert self._application is not None
27
+ self._scheduler_component.configure(
28
+ redis_connection_string=self._redis_credentials_config.url, app=self._application.get_asgi_app()
29
+ )
30
+ self._add_to_state(key=DEPENDS_SCHEDULER_COMPONENT_KEY, value=self._scheduler_component)
31
+ self._register_hook(self._scheduler_component)
32
+
33
+ async def on_startup(self) -> None:
34
+ """On startup."""
35
+ assert self._application is not None
36
+ await self._scheduler_component.startup(app=self._application.get_asgi_app())
37
+
38
+ async def on_shutdown(self) -> None:
39
+ """On shutdown."""
40
+ assert self._application is not None
41
+ await self._scheduler_component.shutdown()