sqlalchemy-foundation-kit 0.0.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.
- sqlalchemy_foundation_kit/__init__.py +119 -0
- sqlalchemy_foundation_kit/__version__.py +1 -0
- sqlalchemy_foundation_kit/_typing.py +28 -0
- sqlalchemy_foundation_kit/base/__init__.py +46 -0
- sqlalchemy_foundation_kit/base/_optional.py +37 -0
- sqlalchemy_foundation_kit/base/engine.py +256 -0
- sqlalchemy_foundation_kit/base/metadata.py +57 -0
- sqlalchemy_foundation_kit/base/models.py +101 -0
- sqlalchemy_foundation_kit/base/serialization.py +98 -0
- sqlalchemy_foundation_kit/base/types.py +72 -0
- sqlalchemy_foundation_kit/config/__init__.py +17 -0
- sqlalchemy_foundation_kit/config/postgres.py +177 -0
- sqlalchemy_foundation_kit/contrib/__init__.py +5 -0
- sqlalchemy_foundation_kit/contrib/_metrics_utils.py +19 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +37 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +196 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +85 -0
- sqlalchemy_foundation_kit/contrib/di/__init__.py +21 -0
- sqlalchemy_foundation_kit/contrib/di/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/di/_deps.py +32 -0
- sqlalchemy_foundation_kit/contrib/di/database.py +169 -0
- sqlalchemy_foundation_kit/contrib/di/metrics.py +71 -0
- sqlalchemy_foundation_kit/contrib/metrics/__init__.py +7 -0
- sqlalchemy_foundation_kit/contrib/metrics/postgres.py +149 -0
- sqlalchemy_foundation_kit/contrib/settings/__init__.py +19 -0
- sqlalchemy_foundation_kit/contrib/settings/postgres.py +183 -0
- sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +17 -0
- sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +101 -0
- sqlalchemy_foundation_kit/contrib/telemetry/uow.py +227 -0
- sqlalchemy_foundation_kit/protocols/__init__.py +21 -0
- sqlalchemy_foundation_kit/protocols/metrics.py +77 -0
- sqlalchemy_foundation_kit/py.typed +0 -0
- sqlalchemy_foundation_kit/session/__init__.py +27 -0
- sqlalchemy_foundation_kit/session/builder.py +293 -0
- sqlalchemy_foundation_kit/session/connection.py +33 -0
- sqlalchemy_foundation_kit/session/factories.py +104 -0
- sqlalchemy_foundation_kit/session/locks.py +68 -0
- sqlalchemy_foundation_kit/session/manager.py +252 -0
- sqlalchemy_foundation_kit/session/retry.py +82 -0
- sqlalchemy_foundation_kit/uow/__init__.py +21 -0
- sqlalchemy_foundation_kit/uow/enums.py +18 -0
- sqlalchemy_foundation_kit/uow/protocols.py +80 -0
- sqlalchemy_foundation_kit/uow/sqlalchemy.py +406 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/METADATA +624 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/RECORD +49 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/WHEEL +4 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Metrics containers for dependency-injector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from ...protocols import PostgresMetricsProtocol
|
|
8
|
+
from .._metrics_utils import _infra_metrics_prefix
|
|
9
|
+
from ._base import BaseDIContainer
|
|
10
|
+
from ._deps import providers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class PrometheusMetricsSettingsProtocol(Protocol):
|
|
15
|
+
"""Protocol for general prometheus metrics settings."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def prefix(self) -> str | None:
|
|
19
|
+
"""Metric prefix."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def enabled(self) -> bool:
|
|
24
|
+
"""Whether metrics are enabled."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class PostgresMetricsSettingsProtocol(Protocol):
|
|
30
|
+
"""Protocol for Postgres settings that have metrics_enabled flag."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def metrics_enabled(self) -> bool:
|
|
34
|
+
"""Whether infrastructure metrics are enabled."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from ...contrib.metrics import PostgresMetrics
|
|
40
|
+
|
|
41
|
+
def _create_postgres_metrics(
|
|
42
|
+
metrics_settings: PrometheusMetricsSettingsProtocol,
|
|
43
|
+
default_prefix: str | None,
|
|
44
|
+
postgres_settings: PostgresMetricsSettingsProtocol | None,
|
|
45
|
+
) -> PostgresMetricsProtocol | None:
|
|
46
|
+
"""Create Postgres metrics if enabled."""
|
|
47
|
+
if postgres_settings is None or not postgres_settings.metrics_enabled:
|
|
48
|
+
return None
|
|
49
|
+
return PostgresMetrics(prefix=_infra_metrics_prefix(default_prefix))
|
|
50
|
+
|
|
51
|
+
class PrometheusMetricsContainer(BaseDIContainer):
|
|
52
|
+
"""Container for Prometheus PostgreSQL metrics.
|
|
53
|
+
|
|
54
|
+
Provides:
|
|
55
|
+
- postgres_metrics: PostgreSQL metrics collector implementing PostgresMetricsProtocol.
|
|
56
|
+
|
|
57
|
+
Configuration:
|
|
58
|
+
- metrics_settings: General prometheus metrics settings (PrometheusMetricsSettingsProtocol).
|
|
59
|
+
- default_prefix: Default prefix for infrastructure metrics (str | None).
|
|
60
|
+
- postgres_settings: PostgreSQL settings with metrics_enabled flag (PostgresMetricsSettingsProtocol).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Configuration
|
|
64
|
+
metrics_settings = providers.Dependency() # type: ignore[misc,var-annotated]
|
|
65
|
+
default_prefix = providers.Dependency() # type: ignore[misc,var-annotated]
|
|
66
|
+
postgres_settings = providers.Dependency(default=None) # type: ignore[misc,var-annotated]
|
|
67
|
+
|
|
68
|
+
# Postgres metrics
|
|
69
|
+
postgres_metrics = providers.Singleton( # type: ignore[misc,var-annotated]
|
|
70
|
+
_create_postgres_metrics,
|
|
71
|
+
metrics_settings=metrics_settings,
|
|
72
|
+
default_prefix=default_prefix,
|
|
73
|
+
postgres_settings=postgres_settings,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
except ImportError: # pragma: no cover
|
|
77
|
+
PrometheusMetricsContainer = None # type: ignore[misc,assignment]
|
|
78
|
+
_create_postgres_metrics = None # type: ignore[misc,assignment]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"PostgresMetricsSettingsProtocol",
|
|
83
|
+
"PrometheusMetricsContainer",
|
|
84
|
+
"PrometheusMetricsSettingsProtocol",
|
|
85
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Dishka dependency injection providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._base import BaseDishkaProvider
|
|
6
|
+
from .database import (
|
|
7
|
+
AsyncDatabaseProvider,
|
|
8
|
+
AsyncUnitOfWorkProvider,
|
|
9
|
+
RetryConfig,
|
|
10
|
+
retry_async_connection,
|
|
11
|
+
)
|
|
12
|
+
from .metrics import PrometheusPostgresMetricsProvider
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AsyncDatabaseProvider",
|
|
16
|
+
"AsyncUnitOfWorkProvider",
|
|
17
|
+
"BaseDishkaProvider",
|
|
18
|
+
"PrometheusPostgresMetricsProvider",
|
|
19
|
+
"RetryConfig",
|
|
20
|
+
"retry_async_connection",
|
|
21
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Base Dishka provider with automatic dependency checking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._deps import Provider, check_dishka
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseDishkaProvider(Provider):
|
|
9
|
+
"""Base provider that checks dishka availability on subclass creation.
|
|
10
|
+
|
|
11
|
+
All Dishka providers should inherit from this class instead of directly
|
|
12
|
+
from dishka.Provider. This ensures consistent error messages when dishka
|
|
13
|
+
is not installed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
17
|
+
"""Check dishka availability when creating a subclass."""
|
|
18
|
+
super().__init_subclass__(**kwargs)
|
|
19
|
+
check_dishka()
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""Check dishka availability when instantiating."""
|
|
23
|
+
check_dishka()
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ["BaseDishkaProvider"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared dishka dependency helpers.
|
|
2
|
+
|
|
3
|
+
Centralizes the dishka import boilerplate and availability check so each
|
|
4
|
+
provider module doesn't have to repeat it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from dishka import Provider, Scope, provide
|
|
11
|
+
|
|
12
|
+
HAS_DISHKA = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
HAS_DISHKA = False
|
|
15
|
+
Provider = object # type: ignore[misc,assignment]
|
|
16
|
+
Scope = None # type: ignore[misc,assignment]
|
|
17
|
+
provide = None # type: ignore[misc,assignment]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_dishka() -> None:
|
|
21
|
+
"""Raise ImportError if dishka is not installed.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ImportError: If dishka is not available.
|
|
25
|
+
"""
|
|
26
|
+
if not HAS_DISHKA:
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"dishka is required for providers. Install it with: pip install 'sqlalchemy-foundation-kit[dishka]'"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["HAS_DISHKA", "Provider", "Scope", "check_dishka", "provide"]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Database providers for dishka."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
11
|
+
|
|
12
|
+
from ...config import PostgresSettingsProtocol
|
|
13
|
+
from ...protocols import PostgresMetricsProtocol
|
|
14
|
+
from ...session import (
|
|
15
|
+
DEFAULT_HEALTHCHECK_QUERY,
|
|
16
|
+
DEFAULT_RETRY_CONFIG,
|
|
17
|
+
AsyncSessionManager,
|
|
18
|
+
RetryConfig,
|
|
19
|
+
create_async_session_manager,
|
|
20
|
+
retry_async_connection,
|
|
21
|
+
)
|
|
22
|
+
from ...uow import (
|
|
23
|
+
AsyncSQLAlchemyUnitOfWork,
|
|
24
|
+
AsyncSQLAlchemyUowTransaction,
|
|
25
|
+
AsyncUnitOfWork,
|
|
26
|
+
)
|
|
27
|
+
from ._base import BaseDishkaProvider
|
|
28
|
+
from ._deps import Scope, provide
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AsyncDatabaseProvider(BaseDishkaProvider):
|
|
34
|
+
"""Provider for database dependencies.
|
|
35
|
+
|
|
36
|
+
Provides:
|
|
37
|
+
- ``AsyncSessionManager``: Manages database connections and engine lifecycle.
|
|
38
|
+
- ``async_sessionmaker[AsyncSession]``: Factory for creating database sessions.
|
|
39
|
+
|
|
40
|
+
Note: ``PostgresMetricsProtocol`` must be provided by a separate provider
|
|
41
|
+
(e.g., :class:`PrometheusPostgresMetricsProvider`) or registered in the container.
|
|
42
|
+
If you don't want metrics, simply don't register such a provider.
|
|
43
|
+
|
|
44
|
+
Customization:
|
|
45
|
+
- Pass ``healthcheck_query=None`` to skip the startup connectivity check.
|
|
46
|
+
- Pass a custom :class:`RetryConfig` to tune startup retry behaviour.
|
|
47
|
+
- Subclass and override :meth:`create_session_manager` to fully customize
|
|
48
|
+
how the manager is constructed (e.g., to pass ``extra_server_settings``,
|
|
49
|
+
``connection_class``, or ``on_engine_created``).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
scope = Scope.APP
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
healthcheck_query: str | None = DEFAULT_HEALTHCHECK_QUERY,
|
|
57
|
+
retry_config: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize provider.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
healthcheck_query: SQL executed at startup to verify connectivity.
|
|
63
|
+
Pass ``None`` to skip the healthcheck entirely.
|
|
64
|
+
retry_config: Retry behavior for the startup healthcheck.
|
|
65
|
+
"""
|
|
66
|
+
super().__init__()
|
|
67
|
+
self._healthcheck_query = healthcheck_query
|
|
68
|
+
self._retry_config = retry_config
|
|
69
|
+
|
|
70
|
+
def create_session_manager(
|
|
71
|
+
self,
|
|
72
|
+
postgres_config: PostgresSettingsProtocol,
|
|
73
|
+
metrics: PostgresMetricsProtocol | None,
|
|
74
|
+
) -> AsyncSessionManager[AsyncSession]:
|
|
75
|
+
"""Build an ``AsyncSessionManager`` for the given config.
|
|
76
|
+
|
|
77
|
+
Override this hook to customize session manager construction — for example,
|
|
78
|
+
to pass ``extra_server_settings``, a custom ``connection_class``, or an
|
|
79
|
+
``on_engine_created`` callback for OpenTelemetry instrumentation.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
postgres_config: PostgreSQL configuration.
|
|
83
|
+
metrics: Optional metrics collector.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Configured ``AsyncSessionManager``.
|
|
87
|
+
"""
|
|
88
|
+
return create_async_session_manager(postgres_config, metrics=metrics)
|
|
89
|
+
|
|
90
|
+
@provide
|
|
91
|
+
async def get_session_manager(
|
|
92
|
+
self,
|
|
93
|
+
postgres_config: PostgresSettingsProtocol,
|
|
94
|
+
metrics: PostgresMetricsProtocol | None = None,
|
|
95
|
+
) -> AsyncIterator[AsyncSessionManager[AsyncSession]]:
|
|
96
|
+
"""Provide database session manager."""
|
|
97
|
+
manager = self.create_session_manager(postgres_config, metrics)
|
|
98
|
+
|
|
99
|
+
if self._healthcheck_query is not None:
|
|
100
|
+
query = self._healthcheck_query
|
|
101
|
+
|
|
102
|
+
async def test_connection() -> None:
|
|
103
|
+
async with manager.session_maker() as session:
|
|
104
|
+
await session.execute(text(query))
|
|
105
|
+
|
|
106
|
+
await retry_async_connection(
|
|
107
|
+
connect_func=test_connection,
|
|
108
|
+
service_name="PostgreSQL",
|
|
109
|
+
config=self._retry_config,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
yield manager
|
|
114
|
+
finally:
|
|
115
|
+
try:
|
|
116
|
+
await manager.aclose()
|
|
117
|
+
logger.info("Database session manager closed successfully")
|
|
118
|
+
except SQLAlchemyError as e:
|
|
119
|
+
logger.warning("Error closing database session manager: %s", e)
|
|
120
|
+
|
|
121
|
+
@provide
|
|
122
|
+
def get_session_maker(self, session_manager: AsyncSessionManager[AsyncSession]) -> async_sessionmaker[AsyncSession]:
|
|
123
|
+
"""Provide session maker."""
|
|
124
|
+
return session_manager.session_maker # type: ignore[no-any-return]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AsyncUnitOfWorkProvider(BaseDishkaProvider):
|
|
128
|
+
"""Provider for Unit of Work.
|
|
129
|
+
|
|
130
|
+
Provides:
|
|
131
|
+
- ``AsyncUnitOfWork``: Standardized interface for database transactions.
|
|
132
|
+
|
|
133
|
+
Note: UoW is APP-scoped because it's stateless — it only holds a reference to
|
|
134
|
+
``session_maker`` (factory). Real database connections are created only when calling
|
|
135
|
+
``uow.transaction()``, and are properly closed after the context manager exits.
|
|
136
|
+
|
|
137
|
+
Customization:
|
|
138
|
+
Override :meth:`create_uow` to use a custom transaction class
|
|
139
|
+
(e.g., one that exposes domain repositories as lazy properties).
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
scope = Scope.APP
|
|
143
|
+
|
|
144
|
+
def create_uow(
|
|
145
|
+
self,
|
|
146
|
+
session_maker: async_sessionmaker[AsyncSession],
|
|
147
|
+
) -> AsyncUnitOfWork[AsyncSQLAlchemyUowTransaction]:
|
|
148
|
+
"""Construct the UoW instance. Override to inject a custom transaction class."""
|
|
149
|
+
return AsyncSQLAlchemyUnitOfWork(session_maker, transaction_factory=AsyncSQLAlchemyUowTransaction)
|
|
150
|
+
|
|
151
|
+
@provide
|
|
152
|
+
def get_uow(
|
|
153
|
+
self,
|
|
154
|
+
session_maker: async_sessionmaker[AsyncSession],
|
|
155
|
+
) -> AsyncUnitOfWork[AsyncSQLAlchemyUowTransaction]:
|
|
156
|
+
"""Provide Unit of Work.
|
|
157
|
+
|
|
158
|
+
Returns a stateless UoW instance that can be safely reused.
|
|
159
|
+
Each call to ``uow.transaction()`` creates a new database session/connection.
|
|
160
|
+
"""
|
|
161
|
+
return self.create_uow(session_maker)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = [
|
|
165
|
+
"AsyncDatabaseProvider",
|
|
166
|
+
"AsyncUnitOfWorkProvider",
|
|
167
|
+
"RetryConfig",
|
|
168
|
+
"retry_async_connection",
|
|
169
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Metrics providers for dishka."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from ...protocols import PostgresMetricsProtocol
|
|
8
|
+
from .._metrics_utils import _infra_metrics_prefix
|
|
9
|
+
from ._base import BaseDishkaProvider
|
|
10
|
+
from ._deps import Scope, provide
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class PrometheusMetricsSettingsProtocol(Protocol):
|
|
15
|
+
"""Protocol for general prometheus metrics settings."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def prefix(self) -> str | None:
|
|
19
|
+
"""Metric prefix."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def enabled(self) -> bool:
|
|
24
|
+
"""Whether metrics are enabled."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class PostgresMetricsSettingsProtocol(Protocol):
|
|
30
|
+
"""Protocol for Postgres settings that have metrics_enabled flag."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def metrics_enabled(self) -> bool:
|
|
34
|
+
"""Whether infrastructure metrics are enabled."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseMetricsProvider(BaseDishkaProvider):
|
|
39
|
+
"""Base provider for metrics with helper for optional infra name prefix."""
|
|
40
|
+
|
|
41
|
+
scope = Scope.APP
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from ...contrib.metrics import PostgresMetrics
|
|
46
|
+
|
|
47
|
+
class PrometheusPostgresMetricsProvider(BaseMetricsProvider):
|
|
48
|
+
"""Provider for Prometheus PostgreSQL metrics."""
|
|
49
|
+
|
|
50
|
+
@provide
|
|
51
|
+
def get_metrics(
|
|
52
|
+
self,
|
|
53
|
+
metrics: PrometheusMetricsSettingsProtocol,
|
|
54
|
+
default_prefix: str | None,
|
|
55
|
+
postgres: PostgresMetricsSettingsProtocol | None = None,
|
|
56
|
+
) -> PostgresMetricsProtocol | None:
|
|
57
|
+
"""Provide Postgres metrics implementing PostgresMetricsProtocol."""
|
|
58
|
+
if postgres is None or not postgres.metrics_enabled:
|
|
59
|
+
return None
|
|
60
|
+
return PostgresMetrics(prefix=_infra_metrics_prefix(default_prefix))
|
|
61
|
+
|
|
62
|
+
except ImportError: # pragma: no cover
|
|
63
|
+
PrometheusPostgresMetricsProvider = None # type: ignore[misc,assignment]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"BaseMetricsProvider",
|
|
68
|
+
"PostgresMetricsSettingsProtocol",
|
|
69
|
+
"PrometheusMetricsSettingsProtocol",
|
|
70
|
+
"PrometheusPostgresMetricsProvider",
|
|
71
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Postgres metrics using prometheus-client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from prometheus_client import Counter, Gauge, Histogram
|
|
9
|
+
|
|
10
|
+
HAS_PROMETHEUS = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_PROMETHEUS = False
|
|
13
|
+
|
|
14
|
+
# Default buckets for connection checkout duration (seconds)
|
|
15
|
+
CONNECTION_CHECKOUT_BUCKETS: tuple[float, ...] = (
|
|
16
|
+
0.001,
|
|
17
|
+
0.005,
|
|
18
|
+
0.01,
|
|
19
|
+
0.025,
|
|
20
|
+
0.05,
|
|
21
|
+
0.1,
|
|
22
|
+
0.25,
|
|
23
|
+
0.5,
|
|
24
|
+
1.0,
|
|
25
|
+
2.5,
|
|
26
|
+
5.0,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _check_prometheus() -> None:
|
|
31
|
+
"""Check if prometheus-client is installed."""
|
|
32
|
+
if not HAS_PROMETHEUS:
|
|
33
|
+
raise ImportError(
|
|
34
|
+
"prometheus-client is required for PostgresMetrics. "
|
|
35
|
+
"Install it with: pip install 'sqlalchemy-foundation-kit[metrics]'"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PostgresMetrics:
|
|
40
|
+
"""Postgres connection pool metrics.
|
|
41
|
+
|
|
42
|
+
Metrics:
|
|
43
|
+
- postgres_db_pool_size: Current database connection pool size.
|
|
44
|
+
- postgres_db_pool_checked_out: Number of connections currently checked out.
|
|
45
|
+
- postgres_db_pool_overflow: Number of connections over pool_size (within max_overflow).
|
|
46
|
+
- postgres_db_connection_checkout_duration_seconds: Time to acquire connection from pool.
|
|
47
|
+
- postgres_db_connection_timeouts_total: Number of connection checkout timeouts.
|
|
48
|
+
- postgres_db_connection_errors_total: Number of connection errors.
|
|
49
|
+
|
|
50
|
+
Labels:
|
|
51
|
+
- error_type: Type of connection error (for errors_total).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, prefix: str | None = None) -> None:
|
|
55
|
+
"""Initialize postgres metrics.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
prefix: Metric name prefix.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ImportError: If prometheus-client is not installed.
|
|
62
|
+
"""
|
|
63
|
+
_check_prometheus()
|
|
64
|
+
|
|
65
|
+
self.pool_size = Gauge(
|
|
66
|
+
_make_metric_name("postgres_db_pool_size", prefix),
|
|
67
|
+
"Current database connection pool size",
|
|
68
|
+
)
|
|
69
|
+
self.pool_checked_out = Gauge(
|
|
70
|
+
_make_metric_name("postgres_db_pool_checked_out", prefix),
|
|
71
|
+
"Number of database connections currently checked out",
|
|
72
|
+
)
|
|
73
|
+
self.pool_overflow = Gauge(
|
|
74
|
+
_make_metric_name("postgres_db_pool_overflow", prefix),
|
|
75
|
+
"Number of connections over pool_size (within max_overflow)",
|
|
76
|
+
)
|
|
77
|
+
self.connection_checkout_duration = Histogram(
|
|
78
|
+
_make_metric_name("postgres_db_connection_checkout_duration_seconds", prefix),
|
|
79
|
+
"Time to acquire connection from pool",
|
|
80
|
+
buckets=list(CONNECTION_CHECKOUT_BUCKETS),
|
|
81
|
+
)
|
|
82
|
+
self.connection_timeouts_total = Counter(
|
|
83
|
+
_make_metric_name("postgres_db_connection_timeouts_total", prefix),
|
|
84
|
+
"Number of connection checkout timeouts",
|
|
85
|
+
)
|
|
86
|
+
self.connection_errors_total = Counter(
|
|
87
|
+
_make_metric_name("postgres_db_connection_errors_total", prefix),
|
|
88
|
+
"Number of connection errors",
|
|
89
|
+
["error_type"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def record_pool_stats(
|
|
93
|
+
self,
|
|
94
|
+
pool_size: int,
|
|
95
|
+
pool_checked_out: int,
|
|
96
|
+
pool_overflow: int,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Record database connection pool statistics."""
|
|
99
|
+
self.pool_size.set(float(pool_size))
|
|
100
|
+
self.pool_checked_out.set(float(pool_checked_out))
|
|
101
|
+
self.pool_overflow.set(float(pool_overflow))
|
|
102
|
+
|
|
103
|
+
def record_checkout(
|
|
104
|
+
self,
|
|
105
|
+
duration: float,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Record a database connection checkout from the pool."""
|
|
108
|
+
self.connection_checkout_duration.observe(duration)
|
|
109
|
+
|
|
110
|
+
def record_error(
|
|
111
|
+
self,
|
|
112
|
+
error_type: str,
|
|
113
|
+
is_timeout: bool = False,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Record a database connection or execution error."""
|
|
116
|
+
self.connection_errors_total.labels(error_type=error_type).inc()
|
|
117
|
+
if is_timeout:
|
|
118
|
+
self.connection_timeouts_total.inc()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _make_metric_name(name: str, prefix: str | None = None) -> str:
|
|
125
|
+
"""Make metric name with optional prefix.
|
|
126
|
+
|
|
127
|
+
Prefix must follow Prometheus naming conventions (^[a-zA-Z_][a-zA-Z0-9_]*$).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
name: Base metric name.
|
|
131
|
+
prefix: Optional prefix for the metric.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The combined metric name.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If the prefix is invalid.
|
|
138
|
+
"""
|
|
139
|
+
if prefix:
|
|
140
|
+
if not _PREFIX_PATTERN.match(prefix):
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Invalid metric prefix: '{prefix}'. "
|
|
143
|
+
"Prefixes must start with a letter or underscore and contain only letters, numbers, or underscores."
|
|
144
|
+
)
|
|
145
|
+
return f"{prefix}_{name}"
|
|
146
|
+
return name
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["PostgresMetrics"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Pydantic Settings integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .postgres import (
|
|
6
|
+
BasePostgresConfig,
|
|
7
|
+
BasePostgresMigrationsConfig,
|
|
8
|
+
ConnectionSettings,
|
|
9
|
+
PoolSettings,
|
|
10
|
+
QuerySettings,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BasePostgresConfig",
|
|
15
|
+
"BasePostgresMigrationsConfig",
|
|
16
|
+
"ConnectionSettings",
|
|
17
|
+
"PoolSettings",
|
|
18
|
+
"QuerySettings",
|
|
19
|
+
]
|