fastapi-factory-utilities 0.4.2__py3-none-any.whl → 0.6.0__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/v1/sys/health.py +1 -1
- fastapi_factory_utilities/core/app/application.py +22 -26
- fastapi_factory_utilities/core/app/builder.py +8 -32
- fastapi_factory_utilities/core/app/fastapi_builder.py +3 -3
- fastapi_factory_utilities/core/exceptions.py +55 -28
- 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 +23 -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/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 +63 -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 +11 -156
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -121
- 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/protocols.py +1 -54
- fastapi_factory_utilities/core/security/jwt.py +3 -3
- fastapi_factory_utilities/example/app.py +13 -4
- {fastapi_factory_utilities-0.4.2.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/METADATA +6 -3
- {fastapi_factory_utilities-0.4.2.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/RECORD +35 -24
- {fastapi_factory_utilities-0.4.2.dist-info → fastapi_factory_utilities-0.6.0.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 -189
- 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.4.2.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/entry_points.txt +0 -0
- {fastapi_factory_utilities-0.4.2.dist-info → fastapi_factory_utilities-0.6.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Oriented Data Model (ODM) plugin package."""
|
|
2
|
+
|
|
3
|
+
from logging import INFO, Logger, getLogger
|
|
4
|
+
from typing import Any, Self
|
|
5
|
+
|
|
6
|
+
from beanie import Document, init_beanie # pyright: ignore[reportUnknownVariableType]
|
|
7
|
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
8
|
+
from reactivex import Subject
|
|
9
|
+
from structlog.stdlib import BoundLogger, get_logger
|
|
10
|
+
|
|
11
|
+
from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
|
|
12
|
+
from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
|
|
13
|
+
from fastapi_factory_utilities.core.services.status.enums import (
|
|
14
|
+
ComponentTypeEnum,
|
|
15
|
+
HealthStatusEnum,
|
|
16
|
+
ReadinessStatusEnum,
|
|
17
|
+
)
|
|
18
|
+
from fastapi_factory_utilities.core.services.status.services import StatusService
|
|
19
|
+
from fastapi_factory_utilities.core.services.status.types import (
|
|
20
|
+
ComponentInstanceType,
|
|
21
|
+
Status,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .builder import ODMBuilder
|
|
25
|
+
from .configs import ODMConfig
|
|
26
|
+
from .depends import depends_odm_client, depends_odm_database
|
|
27
|
+
from .documents import BaseDocument
|
|
28
|
+
from .exceptions import OperationError, UnableToCreateEntityDueToDuplicateKeyError
|
|
29
|
+
from .helpers import PersistedEntity
|
|
30
|
+
from .repositories import AbstractRepository
|
|
31
|
+
|
|
32
|
+
_logger: BoundLogger = get_logger()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ODMPlugin(PluginAbstract):
|
|
36
|
+
"""ODM plugin."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self, document_models: list[type[Document]] | None = None, odm_config: ODMConfig | None = None
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Initialize the ODM plugin."""
|
|
42
|
+
super().__init__()
|
|
43
|
+
self._component_instance: ComponentInstanceType | None = None
|
|
44
|
+
self._monitoring_subject: Subject[Status] | None = None
|
|
45
|
+
self._document_models: list[type[Document]] | None = document_models
|
|
46
|
+
self._odm_config: ODMConfig | None = odm_config
|
|
47
|
+
self._odm_client: AsyncIOMotorClient[Any] | None = None
|
|
48
|
+
self._odm_database: AsyncIOMotorDatabase[Any] | None = None
|
|
49
|
+
|
|
50
|
+
def set_application(self, application: ApplicationAbstractProtocol) -> Self:
|
|
51
|
+
"""Set the application."""
|
|
52
|
+
self._document_models = self._document_models or application.ODM_DOCUMENT_MODELS
|
|
53
|
+
return super().set_application(application)
|
|
54
|
+
|
|
55
|
+
def on_load(self) -> None:
|
|
56
|
+
"""Actions to perform on load for the ODM plugin."""
|
|
57
|
+
# Configure the pymongo logger to INFO level
|
|
58
|
+
|
|
59
|
+
pymongo_logger: Logger = getLogger("pymongo")
|
|
60
|
+
pymongo_logger.setLevel(INFO)
|
|
61
|
+
_logger.debug("ODM plugin loaded.")
|
|
62
|
+
|
|
63
|
+
def _setup_status(self) -> None:
|
|
64
|
+
assert self._application is not None
|
|
65
|
+
status_service: StatusService = self._application.get_status_service()
|
|
66
|
+
self._component_instance = ComponentInstanceType(
|
|
67
|
+
component_type=ComponentTypeEnum.DATABASE, identifier="MongoDB"
|
|
68
|
+
)
|
|
69
|
+
self._monitoring_subject = status_service.register_component_instance(
|
|
70
|
+
component_instance=self._component_instance
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def _setup_beanie(self) -> None:
|
|
74
|
+
assert self._application is not None
|
|
75
|
+
assert self._odm_database is not None
|
|
76
|
+
assert self._document_models is not None
|
|
77
|
+
assert self._monitoring_subject is not None
|
|
78
|
+
# TODO: Find a better way to initialize beanie with the document models of the concrete application
|
|
79
|
+
# through an hook in the application, a dynamis import ?
|
|
80
|
+
try:
|
|
81
|
+
await init_beanie(
|
|
82
|
+
database=self._odm_database,
|
|
83
|
+
document_models=self._document_models,
|
|
84
|
+
)
|
|
85
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
86
|
+
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
87
|
+
# TODO: Report the error to the status_service
|
|
88
|
+
# this will report the application as unhealthy
|
|
89
|
+
self._monitoring_subject.on_next(
|
|
90
|
+
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def on_startup(self) -> None:
|
|
94
|
+
"""Actions to perform on startup for the ODM plugin."""
|
|
95
|
+
assert self._application is not None
|
|
96
|
+
self._setup_status()
|
|
97
|
+
assert self._monitoring_subject is not None
|
|
98
|
+
assert self._component_instance is not None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
odm_factory: ODMBuilder = ODMBuilder(application=self._application, odm_config=self._odm_config).build_all()
|
|
102
|
+
await odm_factory.wait_ping()
|
|
103
|
+
self._odm_database = odm_factory.odm_database
|
|
104
|
+
self._odm_client = odm_factory.odm_client
|
|
105
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
106
|
+
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
107
|
+
# TODO: Report the error to the status_service
|
|
108
|
+
# this will report the application as unhealthy
|
|
109
|
+
self._monitoring_subject.on_next(
|
|
110
|
+
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
if self._odm_database is None or self._odm_client is None:
|
|
115
|
+
_logger.error(
|
|
116
|
+
f"ODM plugin failed to start. Database: {odm_factory.odm_database} - Client: {odm_factory.odm_client}"
|
|
117
|
+
)
|
|
118
|
+
# TODO: Report the error to the status_service
|
|
119
|
+
# this will report the application as unhealthy
|
|
120
|
+
self._monitoring_subject.on_next(
|
|
121
|
+
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
self._add_to_state(key="odm_client", value=odm_factory.odm_client)
|
|
126
|
+
self._add_to_state(key="odm_database", value=odm_factory.odm_database)
|
|
127
|
+
|
|
128
|
+
await self._setup_beanie()
|
|
129
|
+
|
|
130
|
+
_logger.info(
|
|
131
|
+
f"ODM plugin started. Database: {self._odm_database.name} - "
|
|
132
|
+
f"Client: {self._odm_client.address} - "
|
|
133
|
+
f"Document models: {self._application.ODM_DOCUMENT_MODELS}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self._monitoring_subject.on_next(
|
|
137
|
+
value=Status(health=HealthStatusEnum.HEALTHY, readiness=ReadinessStatusEnum.READY)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def on_shutdown(self) -> None:
|
|
141
|
+
"""Actions to perform on shutdown for the ODM plugin."""
|
|
142
|
+
if self._odm_client is not None:
|
|
143
|
+
self._odm_client.close()
|
|
144
|
+
_logger.debug("ODM plugin shutdown.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__: list[str] = [
|
|
148
|
+
"AbstractRepository",
|
|
149
|
+
"BaseDocument",
|
|
150
|
+
"OperationError",
|
|
151
|
+
"PersistedEntity",
|
|
152
|
+
"UnableToCreateEntityDueToDuplicateKeyError",
|
|
153
|
+
"depends_odm_client",
|
|
154
|
+
"depends_odm_database",
|
|
155
|
+
]
|
|
@@ -1,130 +1,17 @@
|
|
|
1
1
|
"""OpenTelemetry Plugin Module."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from typing import cast
|
|
5
|
-
|
|
6
|
-
from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
|
|
7
|
-
from opentelemetry.instrumentation.fastapi import ( # pyright: ignore[reportMissingTypeStubs]
|
|
8
|
-
FastAPIInstrumentor,
|
|
9
|
-
)
|
|
10
|
-
from opentelemetry.sdk.metrics import MeterProvider
|
|
11
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
12
|
-
from structlog.stdlib import BoundLogger, get_logger
|
|
13
|
-
|
|
14
|
-
from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
|
|
15
|
-
|
|
16
|
-
from .builder import OpenTelemetryPluginBuilder
|
|
17
|
-
from .configs import OpenTelemetryConfig
|
|
3
|
+
from .configs import OpenTelemetryConfig, OpenTelemetryMeterConfig, OpenTelemetryTracerConfig
|
|
18
4
|
from .exceptions import OpenTelemetryPluginBaseException, OpenTelemetryPluginConfigError
|
|
5
|
+
from .plugins import OpenTelemetryPlugin, depends_meter_provider, depends_otel_config, depends_tracer_provider
|
|
19
6
|
|
|
20
7
|
__all__: list[str] = [
|
|
21
8
|
"OpenTelemetryConfig",
|
|
9
|
+
"OpenTelemetryMeterConfig",
|
|
10
|
+
"OpenTelemetryPlugin",
|
|
22
11
|
"OpenTelemetryPluginBaseException",
|
|
23
|
-
"OpenTelemetryPluginBuilder",
|
|
24
12
|
"OpenTelemetryPluginConfigError",
|
|
13
|
+
"OpenTelemetryTracerConfig",
|
|
14
|
+
"depends_meter_provider",
|
|
15
|
+
"depends_otel_config",
|
|
16
|
+
"depends_tracer_provider",
|
|
25
17
|
]
|
|
26
|
-
|
|
27
|
-
_logger: BoundLogger = get_logger()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
|
|
31
|
-
"""Check the pre-conditions for the OpenTelemetry plugin.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
application (BaseApplicationProtocol): The application.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
bool: True if the pre-conditions are met, False otherwise.
|
|
38
|
-
"""
|
|
39
|
-
del application
|
|
40
|
-
return True
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def on_load(
|
|
44
|
-
application: ApplicationAbstractProtocol,
|
|
45
|
-
) -> None:
|
|
46
|
-
"""Actions to perform on load for the OpenTelemetry plugin.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
application (BaseApplicationProtocol): The application.
|
|
50
|
-
"""
|
|
51
|
-
# Build the OpenTelemetry Resources, TracerProvider and MeterProvider
|
|
52
|
-
try:
|
|
53
|
-
otel_builder: OpenTelemetryPluginBuilder = OpenTelemetryPluginBuilder(application=application).build_all()
|
|
54
|
-
except OpenTelemetryPluginBaseException as exception:
|
|
55
|
-
_logger.error(f"OpenTelemetry plugin failed to start. {exception}")
|
|
56
|
-
return
|
|
57
|
-
# Configuration is never None at this point (checked in the builder and raises an exception)
|
|
58
|
-
otel_config: OpenTelemetryConfig = cast(OpenTelemetryConfig, otel_builder.config)
|
|
59
|
-
# Save as state in the FastAPI application
|
|
60
|
-
application.get_asgi_app().state.tracer_provider = otel_builder.tracer_provider
|
|
61
|
-
application.get_asgi_app().state.meter_provider = otel_builder.meter_provider
|
|
62
|
-
application.get_asgi_app().state.otel_config = otel_config
|
|
63
|
-
# Instrument the FastAPI application
|
|
64
|
-
FastAPIInstrumentor.instrument_app( # pyright: ignore[reportUnknownMemberType]
|
|
65
|
-
app=application.get_asgi_app(),
|
|
66
|
-
tracer_provider=otel_builder.tracer_provider,
|
|
67
|
-
meter_provider=otel_builder.meter_provider,
|
|
68
|
-
excluded_urls=otel_config.excluded_urls,
|
|
69
|
-
)
|
|
70
|
-
# Instrument the AioHttpClient
|
|
71
|
-
AioHttpClientInstrumentor().instrument( # pyright: ignore[reportUnknownMemberType]
|
|
72
|
-
tracer_provider=otel_builder.tracer_provider,
|
|
73
|
-
meter_provider=otel_builder.meter_provider,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
_logger.debug(f"OpenTelemetry plugin loaded. {otel_config.activate=}")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
async def on_startup(
|
|
80
|
-
application: ApplicationAbstractProtocol,
|
|
81
|
-
) -> None:
|
|
82
|
-
"""Actions to perform on startup for the OpenTelemetry plugin.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
application (BaseApplicationProtocol): The application.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
None
|
|
89
|
-
"""
|
|
90
|
-
del application
|
|
91
|
-
_logger.debug("OpenTelemetry plugin started.")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
|
|
95
|
-
"""Actions to perform on shutdown for the OpenTelemetry plugin.
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
application (BaseApplicationProtocol): The application.
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
None
|
|
102
|
-
"""
|
|
103
|
-
tracer_provider: TracerProvider = application.get_asgi_app().state.tracer_provider
|
|
104
|
-
meter_provider: MeterProvider = application.get_asgi_app().state.meter_provider
|
|
105
|
-
otel_config: OpenTelemetryConfig = application.get_asgi_app().state.otel_config
|
|
106
|
-
|
|
107
|
-
seconds_to_ms_multiplier: int = 1000
|
|
108
|
-
|
|
109
|
-
async def close_tracer_provider() -> None:
|
|
110
|
-
"""Close the tracer provider."""
|
|
111
|
-
tracer_provider.force_flush(timeout_millis=otel_config.closing_timeout * seconds_to_ms_multiplier)
|
|
112
|
-
# No Delay for the shutdown of the tracer provider
|
|
113
|
-
tracer_provider.shutdown()
|
|
114
|
-
|
|
115
|
-
async def close_meter_provider() -> None:
|
|
116
|
-
"""Close the meter provider.
|
|
117
|
-
|
|
118
|
-
Split the timeout in half for the flush and shutdown.
|
|
119
|
-
"""
|
|
120
|
-
meter_provider.force_flush(timeout_millis=int(otel_config.closing_timeout / 2) * seconds_to_ms_multiplier)
|
|
121
|
-
meter_provider.shutdown(timeout_millis=int(otel_config.closing_timeout / 2) * seconds_to_ms_multiplier)
|
|
122
|
-
|
|
123
|
-
_logger.debug("OpenTelemetry plugin stop requested. Flushing and closing...")
|
|
124
|
-
|
|
125
|
-
await asyncio.gather(
|
|
126
|
-
close_tracer_provider(),
|
|
127
|
-
close_meter_provider(),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
_logger.debug("OpenTelemetry plugin closed.")
|
|
@@ -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
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
"""Protocols for the base application."""
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
|
-
from typing import TYPE_CHECKING, ClassVar, Protocol
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol
|
|
5
5
|
|
|
6
6
|
from beanie import Document
|
|
7
7
|
from fastapi import FastAPI
|
|
8
8
|
|
|
9
|
-
from fastapi_factory_utilities.core.plugins import PluginsEnum
|
|
10
9
|
from fastapi_factory_utilities.core.services.status.services import StatusService
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
12
|
from fastapi_factory_utilities.core.app.config import RootConfig
|
|
14
|
-
from fastapi_factory_utilities.core.plugins import PluginState
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class ApplicationAbstractProtocol(Protocol):
|
|
@@ -21,8 +19,6 @@ class ApplicationAbstractProtocol(Protocol):
|
|
|
21
19
|
|
|
22
20
|
ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]]
|
|
23
21
|
|
|
24
|
-
DEFAULT_PLUGINS_ACTIVATED: ClassVar[list[PluginsEnum]]
|
|
25
|
-
|
|
26
22
|
@abstractmethod
|
|
27
23
|
def get_config(self) -> "RootConfig":
|
|
28
24
|
"""Get the application configuration."""
|
|
@@ -34,52 +30,3 @@ class ApplicationAbstractProtocol(Protocol):
|
|
|
34
30
|
@abstractmethod
|
|
35
31
|
def get_status_service(self) -> StatusService:
|
|
36
32
|
"""Get the status service."""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@runtime_checkable
|
|
40
|
-
class PluginProtocol(Protocol):
|
|
41
|
-
"""Defines the protocol for the plugins."""
|
|
42
|
-
|
|
43
|
-
@abstractmethod
|
|
44
|
-
def pre_conditions_check(self, application: ApplicationAbstractProtocol) -> bool:
|
|
45
|
-
"""Check the pre-conditions for the plugin.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
application (BaseApplicationProtocol): The application.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
bool: True if the pre-conditions are met, False otherwise.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
@abstractmethod
|
|
55
|
-
def on_load(self, application: ApplicationAbstractProtocol) -> list["PluginState"] | None:
|
|
56
|
-
"""The actions to perform on load for the plugin.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
application (BaseApplicationProtocol): The application.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
None
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
@abstractmethod
|
|
66
|
-
async def on_startup(self, application: ApplicationAbstractProtocol) -> list["PluginState"] | None:
|
|
67
|
-
"""The actions to perform on startup for the plugin.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
application (BaseApplicationProtocol): The application.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
None
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
@abstractmethod
|
|
77
|
-
async def on_shutdown(self, application: ApplicationAbstractProtocol) -> None:
|
|
78
|
-
"""The actions to perform on shutdown for the plugin.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
application (BaseApplicationProtocol): The application.
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
None
|
|
85
|
-
"""
|
|
@@ -140,11 +140,11 @@ class JWTBearerAuthentication:
|
|
|
140
140
|
# by the request or by the jwt parameter
|
|
141
141
|
if self.jwt_raw is None and request is None:
|
|
142
142
|
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Missing Credentials")
|
|
143
|
-
|
|
143
|
+
jwt_raw: str
|
|
144
144
|
if self.jwt_raw is None:
|
|
145
|
-
jwt_raw
|
|
145
|
+
jwt_raw = self._extract_raw_token(request=request) # type: ignore[arg-type]
|
|
146
146
|
else:
|
|
147
|
-
jwt_raw
|
|
147
|
+
jwt_raw = self.jwt_raw
|
|
148
148
|
|
|
149
149
|
# Execute the io bound and cpu bound tasks in parallel
|
|
150
150
|
async with TaskGroup() as tg:
|
|
@@ -7,7 +7,9 @@ from beanie import Document
|
|
|
7
7
|
from fastapi_factory_utilities.core.app.application import ApplicationAbstract
|
|
8
8
|
from fastapi_factory_utilities.core.app.builder import ApplicationGenericBuilder
|
|
9
9
|
from fastapi_factory_utilities.core.app.config import RootConfig
|
|
10
|
-
from fastapi_factory_utilities.core.plugins import
|
|
10
|
+
from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
|
|
11
|
+
from fastapi_factory_utilities.core.plugins.odm_plugin import ODMPlugin
|
|
12
|
+
from fastapi_factory_utilities.core.plugins.opentelemetry_plugin import OpenTelemetryPlugin
|
|
11
13
|
from fastapi_factory_utilities.example.models.books.document import BookDocument
|
|
12
14
|
|
|
13
15
|
|
|
@@ -26,8 +28,6 @@ class App(ApplicationAbstract):
|
|
|
26
28
|
|
|
27
29
|
ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]] = [BookDocument]
|
|
28
30
|
|
|
29
|
-
DEFAULT_PLUGINS_ACTIVATED: ClassVar[list[PluginsEnum]] = [PluginsEnum.OPENTELEMETRY_PLUGIN, PluginsEnum.ODM_PLUGIN]
|
|
30
|
-
|
|
31
31
|
def configure(self) -> None:
|
|
32
32
|
"""Configure the application."""
|
|
33
33
|
# Prevent circular import
|
|
@@ -48,4 +48,13 @@ class App(ApplicationAbstract):
|
|
|
48
48
|
class AppBuilder(ApplicationGenericBuilder[App]):
|
|
49
49
|
"""Application builder for the App application."""
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
def get_default_plugins(self) -> list[PluginAbstract]:
|
|
52
|
+
"""Get the default plugins."""
|
|
53
|
+
return [ODMPlugin(), OpenTelemetryPlugin()]
|
|
54
|
+
|
|
55
|
+
def __init__(self, plugins: list[PluginAbstract] | None = None) -> None:
|
|
56
|
+
"""Initialize the AppBuilder."""
|
|
57
|
+
# If no plugins are provided, use the default plugins
|
|
58
|
+
if plugins is None:
|
|
59
|
+
plugins = self.get_default_plugins()
|
|
60
|
+
super().__init__(plugins=plugins)
|
{fastapi_factory_utilities-0.4.2.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/METADATA
RENAMED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi_factory_utilities
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Consolidate libraries and utilities to create microservices in Python with FastAPI, Beanie, Httpx, AioPika and OpenTelemetry.
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: python,fastapi,beanie,httpx,opentelemetry,microservices
|
|
7
8
|
Author: miragecentury
|
|
8
9
|
Author-email: victorien.vanroye@gmail.com
|
|
@@ -18,12 +19,14 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
20
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
20
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Dist: aio-pika (>=9.5.7,<10.0.0)
|
|
21
23
|
Requires-Dist: aiohttp[speedups] (>=3.12.13,<4.0.0)
|
|
22
24
|
Requires-Dist: beanie (>=1.30.0,<2.0.0)
|
|
23
25
|
Requires-Dist: fastapi (>=0.115.13,<1)
|
|
24
26
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.26.0,<2.0.0)
|
|
25
27
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.26.0,<2.0.0)
|
|
26
|
-
Requires-Dist: opentelemetry-instrumentation-
|
|
28
|
+
Requires-Dist: opentelemetry-instrumentation-aio-pika (>=0.59b0,<0.60)
|
|
29
|
+
Requires-Dist: opentelemetry-instrumentation-aiohttp-client (>=0,<1)
|
|
27
30
|
Requires-Dist: opentelemetry-instrumentation-fastapi (>=0,<1)
|
|
28
31
|
Requires-Dist: opentelemetry-instrumentation-pymongo (>=0,<1)
|
|
29
32
|
Requires-Dist: opentelemetry-propagator-b3 (>=1.26.0,<2.0.0)
|