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.
- fastapi_factory_utilities/core/api/__init__.py +1 -1
- fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
- fastapi_factory_utilities/core/app/__init__.py +12 -3
- fastapi_factory_utilities/core/app/application.py +24 -26
- fastapi_factory_utilities/core/app/builder.py +23 -37
- fastapi_factory_utilities/core/app/config.py +22 -1
- fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
- fastapi_factory_utilities/core/exceptions.py +58 -22
- fastapi_factory_utilities/core/plugins/__init__.py +2 -31
- fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
- fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
- fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
- fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
- fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
- fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
- fastapi_factory_utilities/core/plugins/aiopika/exchange.py +70 -0
- fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
- fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
- fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
- fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
- fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
- fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
- fastapi_factory_utilities/core/plugins/aiopika/queue.py +86 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +25 -153
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +59 -31
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +2 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +112 -3
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -115
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +65 -14
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +13 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +29 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
- fastapi_factory_utilities/core/protocols.py +1 -54
- fastapi_factory_utilities/core/security/jwt.py +159 -0
- fastapi_factory_utilities/core/security/kratos.py +98 -0
- fastapi_factory_utilities/core/services/hydra/__init__.py +13 -0
- fastapi_factory_utilities/core/services/hydra/exceptions.py +15 -0
- fastapi_factory_utilities/core/services/hydra/objects.py +26 -0
- fastapi_factory_utilities/core/services/hydra/services.py +122 -0
- fastapi_factory_utilities/core/services/kratos/__init__.py +13 -0
- fastapi_factory_utilities/core/services/kratos/enums.py +11 -0
- fastapi_factory_utilities/core/services/kratos/exceptions.py +15 -0
- fastapi_factory_utilities/core/services/kratos/objects.py +43 -0
- fastapi_factory_utilities/core/services/kratos/services.py +86 -0
- fastapi_factory_utilities/core/services/status/__init__.py +2 -2
- fastapi_factory_utilities/core/utils/status.py +2 -1
- fastapi_factory_utilities/core/utils/uvicorn.py +36 -0
- fastapi_factory_utilities/core/utils/yaml_reader.py +2 -2
- fastapi_factory_utilities/example/app.py +15 -5
- fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
- fastapi_factory_utilities/example/models/books/__init__.py +1 -1
- fastapi_factory_utilities/py.typed +0 -0
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/METADATA +23 -14
- fastapi_factory_utilities-0.7.1.dist-info/RECORD +101 -0
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/WHEEL +1 -1
- fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
- fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
- fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
- fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
- fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
- fastapi_factory_utilities-0.2.0.dist-info/RECORD +0 -70
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
"
|
|
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.
|
|
6
|
-
|
|
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 =
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
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()
|