mdb-engine 0.1.6__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.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health check utilities for MDB_ENGINE.
|
|
3
|
+
|
|
4
|
+
Provides health check functions for monitoring system status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from pymongo.errors import (ConnectionFailure, OperationFailure,
|
|
15
|
+
ServerSelectionTimeoutError)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HealthStatus(str, Enum):
|
|
21
|
+
"""Health status enumeration."""
|
|
22
|
+
|
|
23
|
+
HEALTHY = "healthy"
|
|
24
|
+
DEGRADED = "degraded"
|
|
25
|
+
UNHEALTHY = "unhealthy"
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class HealthCheckResult:
|
|
31
|
+
"""Result of a health check."""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
status: HealthStatus
|
|
35
|
+
message: str
|
|
36
|
+
details: Optional[Dict[str, Any]] = None
|
|
37
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
40
|
+
"""Convert to dictionary."""
|
|
41
|
+
return {
|
|
42
|
+
"name": self.name,
|
|
43
|
+
"status": self.status.value,
|
|
44
|
+
"message": self.message,
|
|
45
|
+
"details": self.details,
|
|
46
|
+
"timestamp": self.timestamp.isoformat(),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HealthChecker:
|
|
51
|
+
"""
|
|
52
|
+
Health checker for MDB_ENGINE components.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
"""Initialize the health checker."""
|
|
57
|
+
self._checks: List[callable] = []
|
|
58
|
+
|
|
59
|
+
def register_check(self, check_func: Callable) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Register a health check function.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
check_func: Async function that returns HealthCheckResult
|
|
65
|
+
"""
|
|
66
|
+
self._checks.append(check_func)
|
|
67
|
+
|
|
68
|
+
async def check_all(self) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Run all registered health checks.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with overall status and individual check results
|
|
74
|
+
"""
|
|
75
|
+
results: List[HealthCheckResult] = []
|
|
76
|
+
|
|
77
|
+
for check_func in self._checks:
|
|
78
|
+
try:
|
|
79
|
+
result = await check_func()
|
|
80
|
+
results.append(result)
|
|
81
|
+
except (
|
|
82
|
+
RuntimeError,
|
|
83
|
+
ValueError,
|
|
84
|
+
TypeError,
|
|
85
|
+
AttributeError,
|
|
86
|
+
ConnectionError,
|
|
87
|
+
OSError,
|
|
88
|
+
) as e:
|
|
89
|
+
logger.error(
|
|
90
|
+
f"Health check {check_func.__name__} failed: {e}", exc_info=True
|
|
91
|
+
)
|
|
92
|
+
results.append(
|
|
93
|
+
HealthCheckResult(
|
|
94
|
+
name=check_func.__name__,
|
|
95
|
+
status=HealthStatus.UNKNOWN,
|
|
96
|
+
message=f"Check failed: {str(e)}",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Determine overall status
|
|
101
|
+
statuses = [r.status for r in results]
|
|
102
|
+
if HealthStatus.UNHEALTHY in statuses:
|
|
103
|
+
overall_status = HealthStatus.UNHEALTHY
|
|
104
|
+
elif HealthStatus.DEGRADED in statuses:
|
|
105
|
+
overall_status = HealthStatus.DEGRADED
|
|
106
|
+
elif all(s == HealthStatus.HEALTHY for s in statuses):
|
|
107
|
+
overall_status = HealthStatus.HEALTHY
|
|
108
|
+
else:
|
|
109
|
+
overall_status = HealthStatus.UNKNOWN
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"status": overall_status.value,
|
|
113
|
+
"timestamp": datetime.now().isoformat(),
|
|
114
|
+
"checks": [r.to_dict() for r in results],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def check_mongodb_health(
|
|
119
|
+
mongo_client: Optional[Any], timeout_seconds: float = 5.0
|
|
120
|
+
) -> HealthCheckResult:
|
|
121
|
+
"""
|
|
122
|
+
Check MongoDB connection health.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
mongo_client: MongoDB client instance
|
|
126
|
+
timeout_seconds: Timeout for health check
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
HealthCheckResult
|
|
130
|
+
"""
|
|
131
|
+
if mongo_client is None:
|
|
132
|
+
return HealthCheckResult(
|
|
133
|
+
name="mongodb",
|
|
134
|
+
status=HealthStatus.UNHEALTHY,
|
|
135
|
+
message="MongoDB client not initialized",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Try to ping MongoDB with timeout
|
|
140
|
+
await asyncio.wait_for(
|
|
141
|
+
mongo_client.admin.command("ping"), timeout=timeout_seconds
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return HealthCheckResult(
|
|
145
|
+
name="mongodb",
|
|
146
|
+
status=HealthStatus.HEALTHY,
|
|
147
|
+
message="MongoDB connection is healthy",
|
|
148
|
+
details={"timeout_seconds": timeout_seconds},
|
|
149
|
+
)
|
|
150
|
+
except asyncio.TimeoutError:
|
|
151
|
+
return HealthCheckResult(
|
|
152
|
+
name="mongodb",
|
|
153
|
+
status=HealthStatus.UNHEALTHY,
|
|
154
|
+
message=f"MongoDB ping timed out after {timeout_seconds}s",
|
|
155
|
+
)
|
|
156
|
+
except (
|
|
157
|
+
ConnectionFailure,
|
|
158
|
+
OperationFailure,
|
|
159
|
+
ServerSelectionTimeoutError,
|
|
160
|
+
AttributeError,
|
|
161
|
+
TypeError,
|
|
162
|
+
) as e:
|
|
163
|
+
return HealthCheckResult(
|
|
164
|
+
name="mongodb",
|
|
165
|
+
status=HealthStatus.UNHEALTHY,
|
|
166
|
+
message=f"MongoDB health check failed: {str(e)}",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def check_engine_health(engine: Optional[Any]) -> HealthCheckResult:
|
|
171
|
+
"""
|
|
172
|
+
Check MongoDB Engine health.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
engine: MongoDBEngine instance
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
HealthCheckResult
|
|
179
|
+
"""
|
|
180
|
+
if engine is None:
|
|
181
|
+
return HealthCheckResult(
|
|
182
|
+
name="engine",
|
|
183
|
+
status=HealthStatus.UNHEALTHY,
|
|
184
|
+
message="MongoDBEngine not initialized",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not engine._initialized:
|
|
188
|
+
return HealthCheckResult(
|
|
189
|
+
name="engine",
|
|
190
|
+
status=HealthStatus.UNHEALTHY,
|
|
191
|
+
message="MongoDBEngine not initialized",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Check registered apps
|
|
195
|
+
app_count = len(engine._apps)
|
|
196
|
+
|
|
197
|
+
return HealthCheckResult(
|
|
198
|
+
name="engine",
|
|
199
|
+
status=HealthStatus.HEALTHY,
|
|
200
|
+
message="MongoDBEngine is healthy",
|
|
201
|
+
details={
|
|
202
|
+
"initialized": True,
|
|
203
|
+
"app_count": app_count,
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def check_pool_health(
|
|
209
|
+
get_pool_metrics_func: Optional[Callable[[], Any]] = None
|
|
210
|
+
) -> HealthCheckResult:
|
|
211
|
+
"""
|
|
212
|
+
Check connection pool health.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
get_pool_metrics_func: Function to get pool metrics
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
HealthCheckResult
|
|
219
|
+
"""
|
|
220
|
+
if get_pool_metrics_func is None:
|
|
221
|
+
return HealthCheckResult(
|
|
222
|
+
name="connection_pool",
|
|
223
|
+
status=HealthStatus.UNKNOWN,
|
|
224
|
+
message="Pool metrics function not available",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Call the function to get metrics (handles both sync and async)
|
|
229
|
+
if asyncio.iscoroutinefunction(get_pool_metrics_func):
|
|
230
|
+
metrics = await get_pool_metrics_func()
|
|
231
|
+
else:
|
|
232
|
+
metrics = get_pool_metrics_func()
|
|
233
|
+
|
|
234
|
+
status_value = metrics.get("status")
|
|
235
|
+
if status_value != "connected":
|
|
236
|
+
# "no_client" means shared client not initialized, but engine might use its own client
|
|
237
|
+
# This is not a critical failure, just means pool metrics aren't available
|
|
238
|
+
if status_value == "no_client":
|
|
239
|
+
return HealthCheckResult(
|
|
240
|
+
name="connection_pool",
|
|
241
|
+
status=HealthStatus.UNKNOWN,
|
|
242
|
+
message="Pool metrics not available (engine using dedicated client)",
|
|
243
|
+
details=metrics,
|
|
244
|
+
)
|
|
245
|
+
# "error" means we couldn't get metrics, but the client might still be working
|
|
246
|
+
# This is not a critical failure - the engine and MongoDB checks already
|
|
247
|
+
# verify connectivity
|
|
248
|
+
if status_value == "error":
|
|
249
|
+
return HealthCheckResult(
|
|
250
|
+
name="connection_pool",
|
|
251
|
+
status=HealthStatus.UNKNOWN,
|
|
252
|
+
message=f"Pool metrics unavailable: {metrics.get('error', 'Unknown error')}",
|
|
253
|
+
details=metrics,
|
|
254
|
+
)
|
|
255
|
+
# Other non-connected statuses are still unhealthy
|
|
256
|
+
return HealthCheckResult(
|
|
257
|
+
name="connection_pool",
|
|
258
|
+
status=HealthStatus.UNHEALTHY,
|
|
259
|
+
message=f"Pool status: {status_value}",
|
|
260
|
+
details=metrics,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Client is connected - check usage if available
|
|
264
|
+
usage_percent = metrics.get("pool_usage_percent")
|
|
265
|
+
|
|
266
|
+
# If we have usage data, check it
|
|
267
|
+
if usage_percent is not None:
|
|
268
|
+
if usage_percent > 90:
|
|
269
|
+
status = HealthStatus.UNHEALTHY
|
|
270
|
+
message = f"Connection pool usage is critical: {usage_percent:.1f}%"
|
|
271
|
+
elif usage_percent > 80:
|
|
272
|
+
status = HealthStatus.DEGRADED
|
|
273
|
+
message = f"Connection pool usage is high: {usage_percent:.1f}%"
|
|
274
|
+
else:
|
|
275
|
+
status = HealthStatus.HEALTHY
|
|
276
|
+
message = f"Connection pool is healthy: {usage_percent:.1f}% usage"
|
|
277
|
+
else:
|
|
278
|
+
# No usage data available, but client is connected
|
|
279
|
+
# This is fine - detailed metrics might not be available but client works
|
|
280
|
+
status = HealthStatus.HEALTHY
|
|
281
|
+
message = metrics.get(
|
|
282
|
+
"note", "Connection pool is operational (detailed metrics unavailable)"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return HealthCheckResult(
|
|
286
|
+
name="connection_pool",
|
|
287
|
+
status=status,
|
|
288
|
+
message=message,
|
|
289
|
+
details=metrics,
|
|
290
|
+
)
|
|
291
|
+
except (AttributeError, TypeError, ValueError, KeyError, RuntimeError) as e:
|
|
292
|
+
return HealthCheckResult(
|
|
293
|
+
name="connection_pool",
|
|
294
|
+
status=HealthStatus.UNKNOWN,
|
|
295
|
+
message=f"Failed to check pool health: {str(e)}",
|
|
296
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced logging utilities for MDB_ENGINE.
|
|
3
|
+
|
|
4
|
+
Provides structured logging with correlation IDs and context.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextvars
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
# Context variable for correlation ID
|
|
14
|
+
_correlation_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
15
|
+
"correlation_id", default=None
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Context variable for app context
|
|
19
|
+
_app_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
|
|
20
|
+
"app_context", default=None
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_correlation_id() -> Optional[str]:
|
|
25
|
+
"""Get the current correlation ID from context."""
|
|
26
|
+
return _correlation_id.get()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_correlation_id(correlation_id: Optional[str] = None) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Set a correlation ID in the current context.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
correlation_id: Optional correlation ID (generates new one if None)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The correlation ID that was set
|
|
38
|
+
"""
|
|
39
|
+
if correlation_id is None:
|
|
40
|
+
correlation_id = str(uuid.uuid4())
|
|
41
|
+
_correlation_id.set(correlation_id)
|
|
42
|
+
return correlation_id
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clear_correlation_id() -> None:
|
|
46
|
+
"""Clear the correlation ID from context."""
|
|
47
|
+
_correlation_id.set(None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def set_app_context(app_slug: Optional[str] = None, **kwargs: Any) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Set app context for logging.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
app_slug: App slug
|
|
56
|
+
**kwargs: Additional context (collection_name, user_id, etc.)
|
|
57
|
+
"""
|
|
58
|
+
context = {"app_slug": app_slug, **kwargs}
|
|
59
|
+
_app_context.set(context)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def clear_app_context() -> None:
|
|
63
|
+
"""Clear app context."""
|
|
64
|
+
_app_context.set(None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_logging_context() -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Get current logging context (correlation ID and app context).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dictionary with context information
|
|
73
|
+
"""
|
|
74
|
+
context: Dict[str, Any] = {
|
|
75
|
+
"timestamp": datetime.now().isoformat(),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
correlation_id = get_correlation_id()
|
|
79
|
+
if correlation_id:
|
|
80
|
+
context["correlation_id"] = correlation_id
|
|
81
|
+
|
|
82
|
+
app_context = _app_context.get()
|
|
83
|
+
if app_context:
|
|
84
|
+
context.update(app_context)
|
|
85
|
+
|
|
86
|
+
return context
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ContextualLoggerAdapter(logging.LoggerAdapter):
|
|
90
|
+
"""
|
|
91
|
+
Logger adapter that automatically adds context to log records.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def process(self, msg: str, kwargs: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
|
|
95
|
+
"""Add context to log records."""
|
|
96
|
+
# Get base context
|
|
97
|
+
context = get_logging_context()
|
|
98
|
+
|
|
99
|
+
# Merge with any extra context provided
|
|
100
|
+
extra = kwargs.get("extra", {})
|
|
101
|
+
if extra:
|
|
102
|
+
context.update(extra)
|
|
103
|
+
|
|
104
|
+
kwargs["extra"] = context
|
|
105
|
+
return msg, kwargs
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_logger(name: str) -> ContextualLoggerAdapter:
|
|
109
|
+
"""
|
|
110
|
+
Get a contextual logger that automatically adds correlation ID and context.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: Logger name (typically __name__)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
ContextualLoggerAdapter instance
|
|
117
|
+
"""
|
|
118
|
+
base_logger = logging.getLogger(name)
|
|
119
|
+
return ContextualLoggerAdapter(base_logger, {})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def log_operation(
|
|
123
|
+
logger: logging.Logger,
|
|
124
|
+
operation: str,
|
|
125
|
+
level: int = logging.INFO,
|
|
126
|
+
success: bool = True,
|
|
127
|
+
duration_ms: Optional[float] = None,
|
|
128
|
+
**context: Any,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Log an operation with structured context.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
logger: Logger instance
|
|
135
|
+
operation: Operation name
|
|
136
|
+
level: Log level
|
|
137
|
+
success: Whether operation succeeded
|
|
138
|
+
duration_ms: Operation duration in milliseconds
|
|
139
|
+
**context: Additional context
|
|
140
|
+
"""
|
|
141
|
+
log_context = get_logging_context()
|
|
142
|
+
log_context.update(
|
|
143
|
+
{
|
|
144
|
+
"operation": operation,
|
|
145
|
+
"success": success,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if duration_ms is not None:
|
|
150
|
+
log_context["duration_ms"] = round(duration_ms, 2)
|
|
151
|
+
|
|
152
|
+
if context:
|
|
153
|
+
log_context.update(context)
|
|
154
|
+
|
|
155
|
+
message = f"Operation: {operation}"
|
|
156
|
+
if not success:
|
|
157
|
+
message = f"Operation failed: {operation}"
|
|
158
|
+
if duration_ms is not None:
|
|
159
|
+
message += f" (duration: {duration_ms:.2f}ms)"
|
|
160
|
+
|
|
161
|
+
logger.log(level, message, extra=log_context)
|