fastapi-factory-utilities 0.1.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/__main__.py +6 -0
- fastapi_factory_utilities/core/__init__.py +1 -0
- fastapi_factory_utilities/core/api/__init__.py +25 -0
- fastapi_factory_utilities/core/api/tags.py +9 -0
- fastapi_factory_utilities/core/api/v1/sys/__init__.py +12 -0
- fastapi_factory_utilities/core/api/v1/sys/health.py +53 -0
- fastapi_factory_utilities/core/api/v1/sys/readiness.py +53 -0
- fastapi_factory_utilities/core/app/__init__.py +19 -0
- fastapi_factory_utilities/core/app/base/__init__.py +17 -0
- fastapi_factory_utilities/core/app/base/application.py +123 -0
- fastapi_factory_utilities/core/app/base/config_abstract.py +78 -0
- fastapi_factory_utilities/core/app/base/exceptions.py +25 -0
- fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +88 -0
- fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +136 -0
- fastapi_factory_utilities/core/app/enums.py +11 -0
- fastapi_factory_utilities/core/plugins/__init__.py +15 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +97 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +239 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +17 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +31 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/exceptions.py +25 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +172 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +124 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +266 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +103 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/exceptions.py +13 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/helpers.py +42 -0
- fastapi_factory_utilities/core/protocols.py +82 -0
- fastapi_factory_utilities/core/utils/configs.py +80 -0
- fastapi_factory_utilities/core/utils/importlib.py +28 -0
- fastapi_factory_utilities/core/utils/log.py +178 -0
- fastapi_factory_utilities/core/utils/uvicorn.py +45 -0
- fastapi_factory_utilities/core/utils/yaml_reader.py +166 -0
- fastapi_factory_utilities/example/__init__.py +11 -0
- fastapi_factory_utilities/example/__main__.py +6 -0
- fastapi_factory_utilities/example/api/__init__.py +19 -0
- fastapi_factory_utilities/example/api/books/__init__.py +5 -0
- fastapi_factory_utilities/example/api/books/responses.py +26 -0
- fastapi_factory_utilities/example/api/books/routes.py +62 -0
- fastapi_factory_utilities/example/app/__init__.py +6 -0
- fastapi_factory_utilities/example/app/app.py +37 -0
- fastapi_factory_utilities/example/app/config.py +12 -0
- fastapi_factory_utilities/example/application.yaml +26 -0
- fastapi_factory_utilities/example/entities/books/__init__.py +7 -0
- fastapi_factory_utilities/example/entities/books/entities.py +16 -0
- fastapi_factory_utilities/example/entities/books/enums.py +16 -0
- fastapi_factory_utilities/example/entities/books/types.py +54 -0
- fastapi_factory_utilities/example/models/__init__.py +1 -0
- fastapi_factory_utilities/example/models/books/__init__.py +6 -0
- fastapi_factory_utilities/example/models/books/document.py +20 -0
- fastapi_factory_utilities/example/models/books/repository.py +11 -0
- fastapi_factory_utilities/example/services/books/__init__.py +5 -0
- fastapi_factory_utilities/example/services/books/services.py +167 -0
- fastapi_factory_utilities-0.1.0.dist-info/LICENSE +21 -0
- fastapi_factory_utilities-0.1.0.dist-info/METADATA +131 -0
- fastapi_factory_utilities-0.1.0.dist-info/RECORD +58 -0
- fastapi_factory_utilities-0.1.0.dist-info/WHEEL +4 -0
- fastapi_factory_utilities-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""OpenTelemetry Plugin Module."""
|
|
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 BaseApplicationProtocol
|
|
14
|
+
|
|
15
|
+
from .builder import OpenTelemetryPluginBuilder
|
|
16
|
+
from .configs import OpenTelemetryConfig
|
|
17
|
+
from .exceptions import OpenTelemetryPluginBaseException, OpenTelemetryPluginConfigError
|
|
18
|
+
|
|
19
|
+
__all__: list[str] = [
|
|
20
|
+
"OpenTelemetryConfig",
|
|
21
|
+
"OpenTelemetryPluginBaseException",
|
|
22
|
+
"OpenTelemetryPluginConfigError",
|
|
23
|
+
"OpenTelemetryPluginBuilder",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
_logger: BoundLogger = get_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def pre_conditions_check(application: BaseApplicationProtocol) -> 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: BaseApplicationProtocol,
|
|
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: BaseApplicationProtocol,
|
|
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: BaseApplicationProtocol) -> 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.")
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Provides a factory function to build a objets for OpenTelemetry."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Self
|
|
4
|
+
|
|
5
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
7
|
+
from opentelemetry.metrics import set_meter_provider
|
|
8
|
+
from opentelemetry.propagate import set_global_textmap
|
|
9
|
+
from opentelemetry.propagators.b3 import B3MultiFormat
|
|
10
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
11
|
+
from opentelemetry.sdk.metrics.export import MetricReader, PeriodicExportingMetricReader
|
|
12
|
+
from opentelemetry.sdk.resources import (
|
|
13
|
+
DEPLOYMENT_ENVIRONMENT,
|
|
14
|
+
SERVICE_NAME,
|
|
15
|
+
SERVICE_NAMESPACE,
|
|
16
|
+
SERVICE_VERSION,
|
|
17
|
+
Resource,
|
|
18
|
+
)
|
|
19
|
+
from opentelemetry.sdk.trace import SynchronousMultiSpanProcessor, TracerProvider
|
|
20
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
21
|
+
from opentelemetry.trace import set_tracer_provider
|
|
22
|
+
|
|
23
|
+
from fastapi_factory_utilities.core.plugins.opentelemetry_plugin.configs import (
|
|
24
|
+
OpenTelemetryMeterConfig,
|
|
25
|
+
OpenTelemetryTracerConfig,
|
|
26
|
+
)
|
|
27
|
+
from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
|
|
28
|
+
from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
|
|
29
|
+
from fastapi_factory_utilities.core.utils.yaml_reader import (
|
|
30
|
+
UnableToReadYamlFileError,
|
|
31
|
+
YamlFileReader,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from .configs import OpenTelemetryConfig
|
|
35
|
+
from .exceptions import OpenTelemetryPluginConfigError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OpenTelemetryPluginBuilder:
|
|
39
|
+
"""Configure the injection bindings for OpenTelemetryPlugin."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, application: BaseApplicationProtocol) -> None:
|
|
42
|
+
"""Instantiate the OpenTelemetryPluginFactory.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
application (BaseApplicationProtocol): The application object.
|
|
46
|
+
"""
|
|
47
|
+
self._application: BaseApplicationProtocol = application
|
|
48
|
+
self._resource: Resource | None = None
|
|
49
|
+
self._config: OpenTelemetryConfig | None = None
|
|
50
|
+
self._meter_provider: MeterProvider | None = None
|
|
51
|
+
self._tracer_provider: TracerProvider | None = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def resource(self) -> Resource | None:
|
|
55
|
+
"""Provide the resource object for OpenTelemetry.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Resource | None: The resource object for OpenTelemetry.
|
|
59
|
+
"""
|
|
60
|
+
return self._resource
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def config(self) -> OpenTelemetryConfig | None:
|
|
64
|
+
"""Provide the configuration object for OpenTelemetry.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
OpenTelemetryConfig | None: The configuration object for OpenTelemetry.
|
|
68
|
+
"""
|
|
69
|
+
return self._config
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def meter_provider(self) -> MeterProvider | None:
|
|
73
|
+
"""Provide the meter provider object for OpenTelemetry.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
MeterProvider | None: The meter provider object for OpenTelemetry.
|
|
77
|
+
"""
|
|
78
|
+
return self._meter_provider
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def tracer_provider(self) -> TracerProvider | None:
|
|
82
|
+
"""Provide the tracer provider object for OpenTelemetry.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
TracerProvider | None: The tracer provider object for OpenTelemetry.
|
|
86
|
+
"""
|
|
87
|
+
return self._tracer_provider
|
|
88
|
+
|
|
89
|
+
def build_resource(self) -> Self:
|
|
90
|
+
"""Build a resource object for OpenTelemetry from the application and configs.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Self: The OpenTelemetryPluginFactory object.
|
|
94
|
+
"""
|
|
95
|
+
self._resource = Resource(
|
|
96
|
+
attributes={
|
|
97
|
+
DEPLOYMENT_ENVIRONMENT: self._application.get_config().environment.value,
|
|
98
|
+
SERVICE_NAME: self._application.get_config().service_name,
|
|
99
|
+
SERVICE_NAMESPACE: self._application.get_config().service_namespace,
|
|
100
|
+
SERVICE_VERSION: self._application.get_config().version,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def build_config(
|
|
106
|
+
self,
|
|
107
|
+
) -> Self:
|
|
108
|
+
"""Build the configuration object for OpenTelemetry from the application.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Self: The OpenTelemetryPluginFactory object.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
OpenTelemetryPluginConfigError: If the package name is not set in the application.
|
|
115
|
+
OpenTelemetryPluginConfigError: If the application configuration file is not found.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
if self._application.PACKAGE_NAME == "":
|
|
119
|
+
raise OpenTelemetryPluginConfigError("The package name must be set in the concrete application class.")
|
|
120
|
+
|
|
121
|
+
# Read the application configuration file
|
|
122
|
+
try:
|
|
123
|
+
yaml_file_content: dict[str, Any] = YamlFileReader(
|
|
124
|
+
file_path=get_path_file_in_package(
|
|
125
|
+
filename="application.yaml",
|
|
126
|
+
package=self._application.PACKAGE_NAME,
|
|
127
|
+
),
|
|
128
|
+
yaml_base_key="opentelemetry",
|
|
129
|
+
use_environment_injection=True,
|
|
130
|
+
).read()
|
|
131
|
+
except (FileNotFoundError, ImportError, UnableToReadYamlFileError) as exception:
|
|
132
|
+
raise OpenTelemetryPluginConfigError("Unable to read the application configuration file.") from exception
|
|
133
|
+
|
|
134
|
+
# Create the application configuration model
|
|
135
|
+
try:
|
|
136
|
+
self._config = OpenTelemetryConfig(**yaml_file_content)
|
|
137
|
+
except ValueError as exception:
|
|
138
|
+
raise OpenTelemetryPluginConfigError("Unable to create the application configuration model.") from exception
|
|
139
|
+
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def build_meter_provider(
|
|
143
|
+
self,
|
|
144
|
+
) -> Self:
|
|
145
|
+
"""Build a meter provider object for OpenTelemetry.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Self: The OpenTelemetryPluginFactory object.
|
|
149
|
+
"""
|
|
150
|
+
if self._resource is None:
|
|
151
|
+
raise OpenTelemetryPluginConfigError("The resource object is missing.")
|
|
152
|
+
|
|
153
|
+
if self._config is None:
|
|
154
|
+
raise OpenTelemetryPluginConfigError("The configuration object is missing.")
|
|
155
|
+
|
|
156
|
+
readers: list[MetricReader] = []
|
|
157
|
+
if self._config.activate is True:
|
|
158
|
+
if self._config.meter_config is None:
|
|
159
|
+
# TODO: switch to a custom exception
|
|
160
|
+
raise OpenTelemetryPluginConfigError("The meter configuration is missing.")
|
|
161
|
+
|
|
162
|
+
# TODO: Extract to a dedicated method for the exporter and period reader setup
|
|
163
|
+
|
|
164
|
+
# Setup the Exporter
|
|
165
|
+
exporter = OTLPMetricExporter(
|
|
166
|
+
endpoint=f"{self._config.endpoint.unicode_string()}v1/metrics",
|
|
167
|
+
timeout=self._config.timeout,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Setup the Metric Reader
|
|
171
|
+
meter_config: OpenTelemetryMeterConfig = self._config.meter_config
|
|
172
|
+
reader = PeriodicExportingMetricReader(
|
|
173
|
+
exporter=exporter,
|
|
174
|
+
export_interval_millis=meter_config.reader_interval_millis,
|
|
175
|
+
export_timeout_millis=meter_config.reader_timeout_millis,
|
|
176
|
+
)
|
|
177
|
+
readers.append(reader)
|
|
178
|
+
|
|
179
|
+
# Setup the Meter Provider
|
|
180
|
+
self._meter_provider = MeterProvider(
|
|
181
|
+
resource=self._resource,
|
|
182
|
+
metric_readers=readers,
|
|
183
|
+
shutdown_on_exit=True,
|
|
184
|
+
views=[],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
set_meter_provider(self._meter_provider)
|
|
188
|
+
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def build_tracer_provider(
|
|
192
|
+
self,
|
|
193
|
+
) -> Self:
|
|
194
|
+
"""Provides a tracer provider for OpenTelemetry.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Self: The OpenTelemetryPluginFactory object.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
OpenTelemetryPluginConfigError: If the resource object is missing.
|
|
201
|
+
OpenTelemetryPluginConfigError: If the configuration object is missing.
|
|
202
|
+
"""
|
|
203
|
+
if self._resource is None:
|
|
204
|
+
raise OpenTelemetryPluginConfigError("The resource object is missing.")
|
|
205
|
+
|
|
206
|
+
if self._config is None:
|
|
207
|
+
raise OpenTelemetryPluginConfigError("The configuration object is missing.")
|
|
208
|
+
|
|
209
|
+
active_span_processor: SynchronousMultiSpanProcessor | None = None
|
|
210
|
+
|
|
211
|
+
# Exit with a void TracerProvider if the export is not activated
|
|
212
|
+
if self._config.activate is True:
|
|
213
|
+
if self._config.tracer_config is None:
|
|
214
|
+
raise OpenTelemetryPluginConfigError("The tracer configuration is missing.")
|
|
215
|
+
|
|
216
|
+
# Setup the Exporter
|
|
217
|
+
exporter = OTLPSpanExporter(
|
|
218
|
+
endpoint=f"{self._config.endpoint.unicode_string()}v1/traces",
|
|
219
|
+
timeout=self._config.timeout,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Setup the Span Processor
|
|
223
|
+
tracer_config: OpenTelemetryTracerConfig = self._config.tracer_config
|
|
224
|
+
span_processor = BatchSpanProcessor(
|
|
225
|
+
span_exporter=exporter,
|
|
226
|
+
max_queue_size=tracer_config.max_queue_size,
|
|
227
|
+
max_export_batch_size=tracer_config.max_export_batch_size,
|
|
228
|
+
schedule_delay_millis=tracer_config.schedule_delay_millis,
|
|
229
|
+
export_timeout_millis=tracer_config.export_timeout_millis,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Setup the Multi Span Processor
|
|
233
|
+
active_span_processor = SynchronousMultiSpanProcessor()
|
|
234
|
+
active_span_processor.add_span_processor(span_processor=span_processor)
|
|
235
|
+
|
|
236
|
+
# Setup the TextMap Propagator for B3
|
|
237
|
+
set_global_textmap(http_text_format=B3MultiFormat())
|
|
238
|
+
|
|
239
|
+
# Setup the Tracer Provider
|
|
240
|
+
self._tracer_provider = TracerProvider(
|
|
241
|
+
sampler=None,
|
|
242
|
+
resource=self._resource,
|
|
243
|
+
active_span_processor=active_span_processor,
|
|
244
|
+
id_generator=None,
|
|
245
|
+
span_limits=None,
|
|
246
|
+
shutdown_on_exit=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
set_tracer_provider(self._tracer_provider)
|
|
250
|
+
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def build_all(
|
|
254
|
+
self,
|
|
255
|
+
) -> Self:
|
|
256
|
+
"""Build all the objects for OpenTelemetry.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Self: The OpenTelemetryPluginFactory object.
|
|
260
|
+
"""
|
|
261
|
+
self.build_resource()
|
|
262
|
+
self.build_config()
|
|
263
|
+
self.build_meter_provider()
|
|
264
|
+
self.build_tracer_provider()
|
|
265
|
+
|
|
266
|
+
return self
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Provides the configuration model for the OpenTelemetry plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, UrlConstraints
|
|
6
|
+
from pydantic_core import Url
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenTelemetryMeterConfig(BaseModel):
|
|
10
|
+
"""Provides the configuration model for the OpenTelemetry meter as sub-model."""
|
|
11
|
+
|
|
12
|
+
# Constants for time in milliseconds and seconds
|
|
13
|
+
ONE_MINUTE_IN_MILLIS: int = 60000
|
|
14
|
+
ONE_SECOND_IN_MILLIS: int = 1000
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
17
|
+
|
|
18
|
+
reader_interval_millis: float = Field(
|
|
19
|
+
default=ONE_SECOND_IN_MILLIS,
|
|
20
|
+
description="The interval in miliseconds to read and export metrics.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
reader_timeout_millis: float = Field(
|
|
24
|
+
default=ONE_SECOND_IN_MILLIS,
|
|
25
|
+
description="The timeout in miliseconds for the reader.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenTelemetryTracerConfig(BaseModel):
|
|
30
|
+
"""Provides the configuration model for the OpenTelemetry tracer as sub-model."""
|
|
31
|
+
|
|
32
|
+
# Constants for time in milliseconds and seconds
|
|
33
|
+
FIVE_SECONDS_IN_MILLIS: int = 5000
|
|
34
|
+
THIRTY_SECONDS_IN_MILLIS: int = 30000
|
|
35
|
+
|
|
36
|
+
# Default values for the OpenTelemetry tracer
|
|
37
|
+
DEFAULT_OTEL_TRACER_MAX_QUEUE_SIZE: int = 2048
|
|
38
|
+
DEFAULT_OTEL_TRACER_MAX_EXPORT_BATCH_SIZE: int = 512
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
41
|
+
|
|
42
|
+
max_queue_size: int = Field(
|
|
43
|
+
default=DEFAULT_OTEL_TRACER_MAX_QUEUE_SIZE,
|
|
44
|
+
description="The maximum queue size for the tracer.",
|
|
45
|
+
)
|
|
46
|
+
max_export_batch_size: int = Field(
|
|
47
|
+
default=DEFAULT_OTEL_TRACER_MAX_EXPORT_BATCH_SIZE,
|
|
48
|
+
description="The maximum export batch size for the tracer.",
|
|
49
|
+
)
|
|
50
|
+
schedule_delay_millis: int = Field(
|
|
51
|
+
default=FIVE_SECONDS_IN_MILLIS,
|
|
52
|
+
description="The schedule delay in miliseconds for the tracer.",
|
|
53
|
+
)
|
|
54
|
+
export_timeout_millis: int = Field(
|
|
55
|
+
default=THIRTY_SECONDS_IN_MILLIS,
|
|
56
|
+
description="The export timeout in miliseconds for the tracer.",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OpenTelemetryConfig(BaseModel):
|
|
61
|
+
"""Provides the configuration model for the OpenTelemetry plugin."""
|
|
62
|
+
|
|
63
|
+
# Constants for time in milliseconds and seconds
|
|
64
|
+
TEN_SECONDS_IN_SECONDS: int = 10
|
|
65
|
+
|
|
66
|
+
# Default value for the collector endpoint
|
|
67
|
+
DEFAULT_COLLECTOR_ENDPOINT: str = "http://localhost:4318"
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
70
|
+
|
|
71
|
+
activate: bool = Field(
|
|
72
|
+
default=False,
|
|
73
|
+
description="Whether to activate the OpenTelemetry collector export.",
|
|
74
|
+
)
|
|
75
|
+
endpoint: Annotated[Url, UrlConstraints(allowed_schemes=["http", "https"])] = Field(
|
|
76
|
+
default=Url(url=DEFAULT_COLLECTOR_ENDPOINT),
|
|
77
|
+
description="The collector endpoint.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
timeout: int = Field(
|
|
81
|
+
default=TEN_SECONDS_IN_SECONDS,
|
|
82
|
+
description="The timeout in seconds for the collector.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
closing_timeout: int = Field(
|
|
86
|
+
default=TEN_SECONDS_IN_SECONDS,
|
|
87
|
+
description="The closing timeout in seconds for the collector.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
meter_config: OpenTelemetryMeterConfig | None = Field(
|
|
91
|
+
default_factory=OpenTelemetryMeterConfig,
|
|
92
|
+
description="The meter configuration.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
tracer_config: OpenTelemetryTracerConfig | None = Field(
|
|
96
|
+
default_factory=OpenTelemetryTracerConfig,
|
|
97
|
+
description="The tracer configuration.",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
excluded_urls: list[str] = Field(
|
|
101
|
+
default_factory=list,
|
|
102
|
+
description="The excluded URLs for both the metrics and traces.",
|
|
103
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Provides the exceptions for the OpenTelemetryPlugin."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OpenTelemetryPluginBaseException(BaseException):
|
|
5
|
+
"""Base exception for the OpenTelemetryPlugin."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenTelemetryPluginConfigError(OpenTelemetryPluginBaseException):
|
|
11
|
+
"""Exception for the OpenTelemetryPlugin configuration."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Provides helper functions for the OpenTelemetry plugin."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from opentelemetry.context import Context
|
|
9
|
+
from opentelemetry.trace import SpanKind
|
|
10
|
+
from opentelemetry.util import types
|
|
11
|
+
|
|
12
|
+
Param = ParamSpec("Param")
|
|
13
|
+
RetType = TypeVar("RetType") # pylint: disable=invalid-name
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def trace_span(
|
|
17
|
+
name: str | None = None,
|
|
18
|
+
context: Context | None = None,
|
|
19
|
+
kind: SpanKind = SpanKind.INTERNAL,
|
|
20
|
+
attributes: types.Attributes = None,
|
|
21
|
+
) -> Callable[[Callable[Param, RetType]], Callable[Param, RetType]]:
|
|
22
|
+
"""Decorator to trace a function using OpenTelemetry."""
|
|
23
|
+
|
|
24
|
+
def decorator(func: Callable[Param, RetType]) -> Callable[Param, RetType]:
|
|
25
|
+
@wraps(wrapped=func)
|
|
26
|
+
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> RetType:
|
|
27
|
+
# Get Tracer from the instrumented function's module
|
|
28
|
+
tracer: trace.Tracer = trace.get_tracer(instrumenting_module_name=func.__module__)
|
|
29
|
+
# Use the function's name as the span name if no name is provided
|
|
30
|
+
trace_name: str = name if name is not None else func.__name__
|
|
31
|
+
# Start a span with the provided name
|
|
32
|
+
with tracer.start_as_current_span(
|
|
33
|
+
name=trace_name,
|
|
34
|
+
kind=kind,
|
|
35
|
+
context=context,
|
|
36
|
+
attributes=attributes,
|
|
37
|
+
):
|
|
38
|
+
return func(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
return wrapper
|
|
41
|
+
|
|
42
|
+
return decorator
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Protocols for the base application."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from beanie import Document
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from fastapi_factory_utilities.core.app.base.config_abstract import (
|
|
11
|
+
AppConfigAbstract,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseApplicationProtocol(Protocol):
|
|
16
|
+
"""Protocol for the base application."""
|
|
17
|
+
|
|
18
|
+
PACKAGE_NAME: str
|
|
19
|
+
|
|
20
|
+
ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]]
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def get_config(self) -> "AppConfigAbstract":
|
|
24
|
+
"""Get the application configuration."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def get_asgi_app(self) -> FastAPI:
|
|
28
|
+
"""Get the ASGI application."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class PluginProtocol(Protocol):
|
|
33
|
+
"""Defines the protocol for the plugin.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
INJECTOR_MODULE (type[Module]): The module for the plugin.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def pre_conditions_check(self, application: BaseApplicationProtocol) -> bool:
|
|
42
|
+
"""Check the pre-conditions for the plugin.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
application (BaseApplicationProtocol): The application.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
bool: True if the pre-conditions are met, False otherwise.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def on_load(self, application: BaseApplicationProtocol) -> None:
|
|
53
|
+
"""The actions to perform on load for the plugin.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
application (BaseApplicationProtocol): The application.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
None
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def on_startup(self, application: BaseApplicationProtocol) -> None:
|
|
64
|
+
"""The actions to perform on startup for the plugin.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
application (BaseApplicationProtocol): The application.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
None
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def on_shutdown(self, application: BaseApplicationProtocol) -> None:
|
|
75
|
+
"""The actions to perform on shutdown for the plugin.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
application (BaseApplicationProtocol): The application.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
None
|
|
82
|
+
"""
|