dory-sdk 2.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.
- dory/__init__.py +70 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +290 -0
- dory/cli/templates.py +333 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +50 -0
- dory/config/loader.py +361 -0
- dory/config/presets.py +325 -0
- dory/config/schema.py +152 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +404 -0
- dory/core/context.py +209 -0
- dory/core/lifecycle.py +214 -0
- dory/core/meta.py +121 -0
- dory/core/modes.py +479 -0
- dory/core/processor.py +654 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/errors/__init__.py +117 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +495 -0
- dory/health/__init__.py +10 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +306 -0
- dory/k8s/__init__.py +11 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +175 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +36 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +321 -0
- dory/middleware/request_tracker.py +501 -0
- dory/migration/__init__.py +11 -0
- dory/migration/configmap.py +260 -0
- dory/migration/serialization.py +167 -0
- dory/migration/state_manager.py +301 -0
- dory/monitoring/__init__.py +23 -0
- dory/monitoring/opentelemetry.py +462 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +480 -0
- dory/recovery/golden_snapshot.py +561 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +479 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +187 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +454 -0
- dory/resilience/retry.py +389 -0
- dory/sidecar/__init__.py +6 -0
- dory/sidecar/main.py +75 -0
- dory/sidecar/server.py +329 -0
- dory/simple.py +342 -0
- dory/types.py +75 -0
- dory/utils/__init__.py +25 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_sdk-2.1.0.dist-info/METADATA +663 -0
- dory_sdk-2.1.0.dist-info/RECORD +69 -0
- dory_sdk-2.1.0.dist-info/WHEEL +5 -0
- dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
- dory_sdk-2.1.0.dist-info/top_level.txt +1 -0
dory/core/processor.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BaseProcessor - Abstract base class for processor implementations.
|
|
3
|
+
|
|
4
|
+
Developers implement this class to create their processor applications.
|
|
5
|
+
The SDK handles all lifecycle, state management, and health concerns.
|
|
6
|
+
|
|
7
|
+
SDK v2.1 Auto-Features:
|
|
8
|
+
1. Auto-Initialization - All components created from config:
|
|
9
|
+
- Circuit breakers (self.circuit_breakers)
|
|
10
|
+
- Error classifier (self.error_classifier)
|
|
11
|
+
- OpenTelemetry (self.otel)
|
|
12
|
+
- Request tracker (self.request_tracker)
|
|
13
|
+
- Request ID middleware (self.request_id_middleware)
|
|
14
|
+
- Connection tracker (self.connection_tracker)
|
|
15
|
+
|
|
16
|
+
2. Auto-Instrumentation - All handler methods automatically get:
|
|
17
|
+
- Request ID generation
|
|
18
|
+
- Request tracking
|
|
19
|
+
- OpenTelemetry spans
|
|
20
|
+
- Error classification
|
|
21
|
+
- No decorators needed!
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import logging
|
|
26
|
+
from abc import ABC, abstractmethod
|
|
27
|
+
from typing import TYPE_CHECKING, AsyncIterator, Dict, Any, Optional
|
|
28
|
+
|
|
29
|
+
from dory.decorators import get_stateful_vars, set_stateful_vars
|
|
30
|
+
from dory.core.meta import AutoInstrumentMeta
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from dory.core.context import ExecutionContext
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseProcessor(ABC, metaclass=AutoInstrumentMeta):
|
|
39
|
+
"""
|
|
40
|
+
Abstract base class for processor implementations.
|
|
41
|
+
|
|
42
|
+
Required method:
|
|
43
|
+
- run(): Main processing loop
|
|
44
|
+
|
|
45
|
+
Optional methods (have sensible defaults):
|
|
46
|
+
- startup(): Initialize resources (default: no-op)
|
|
47
|
+
- shutdown(): Cleanup resources (default: no-op)
|
|
48
|
+
- get_state(): Return state dict (default: returns @stateful vars or {})
|
|
49
|
+
- restore_state(): Restore state (default: restores @stateful vars)
|
|
50
|
+
|
|
51
|
+
Optional fault handling hooks:
|
|
52
|
+
- on_state_restore_failed(): Handle state restore errors
|
|
53
|
+
- on_rapid_restart_detected(): Handle restart loop
|
|
54
|
+
- on_health_check_failed(): Handle health check errors
|
|
55
|
+
- reset_caches(): Clean caches during golden image reset
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
# Minimal implementation (just run method)
|
|
59
|
+
class MyProcessor(BaseProcessor):
|
|
60
|
+
counter = stateful(0)
|
|
61
|
+
|
|
62
|
+
async def run(self):
|
|
63
|
+
async for _ in self.run_loop(interval=1):
|
|
64
|
+
self.counter += 1
|
|
65
|
+
|
|
66
|
+
# Full implementation
|
|
67
|
+
class MyProcessor(BaseProcessor):
|
|
68
|
+
async def startup(self):
|
|
69
|
+
self.model = load_model()
|
|
70
|
+
|
|
71
|
+
async def run(self):
|
|
72
|
+
while not self.context.is_shutdown_requested():
|
|
73
|
+
process()
|
|
74
|
+
|
|
75
|
+
async def shutdown(self):
|
|
76
|
+
self.model.close()
|
|
77
|
+
|
|
78
|
+
def get_state(self):
|
|
79
|
+
return {"processed": self.count}
|
|
80
|
+
|
|
81
|
+
async def restore_state(self, state):
|
|
82
|
+
self.count = state.get("processed", 0)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Optional: Define state schema for validation
|
|
86
|
+
# Schema example: {'processed_count': int, 'last_frame_id': int}
|
|
87
|
+
state_schema: dict[str, type] | None = None
|
|
88
|
+
|
|
89
|
+
# Context is auto-injected by DoryApp (no need to accept in __init__)
|
|
90
|
+
context: "ExecutionContext"
|
|
91
|
+
|
|
92
|
+
def __init__(self, context: "ExecutionContext | None" = None):
|
|
93
|
+
"""
|
|
94
|
+
Initialize processor with auto-initialization of SDK components.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
context: ExecutionContext (optional - will be auto-injected if not provided)
|
|
98
|
+
|
|
99
|
+
Auto-Initialized Components (SDK v2.1):
|
|
100
|
+
- self.error_classifier: Automatic error classification
|
|
101
|
+
- self.circuit_breakers: Dict of circuit breakers (database, external_api, cache)
|
|
102
|
+
- self.otel: OpenTelemetry manager (if enabled in config)
|
|
103
|
+
- self.request_tracker: Request tracking middleware (if enabled)
|
|
104
|
+
- self.request_id_middleware: Request ID generation (if enabled)
|
|
105
|
+
- self.connection_tracker: Connection lifecycle tracking (if enabled)
|
|
106
|
+
|
|
107
|
+
Note:
|
|
108
|
+
You can override __init__ and call super().__init__(context) to get
|
|
109
|
+
auto-initialization, or skip super() call to manually initialize.
|
|
110
|
+
"""
|
|
111
|
+
if context is not None:
|
|
112
|
+
self.context = context
|
|
113
|
+
|
|
114
|
+
# Auto-initialize SDK components if context is available
|
|
115
|
+
self._auto_initialize_components()
|
|
116
|
+
|
|
117
|
+
# =========================================================================
|
|
118
|
+
# Required Method
|
|
119
|
+
# =========================================================================
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
async def run(self) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Main processing loop.
|
|
125
|
+
|
|
126
|
+
Called after startup() and restore_state(). Must check
|
|
127
|
+
context.is_shutdown_requested() periodically to exit gracefully.
|
|
128
|
+
|
|
129
|
+
You can use self.run_loop() helper for cleaner code:
|
|
130
|
+
|
|
131
|
+
async def run(self):
|
|
132
|
+
async for _ in self.run_loop(interval=1):
|
|
133
|
+
self.counter += 1
|
|
134
|
+
|
|
135
|
+
Or traditional while loop:
|
|
136
|
+
|
|
137
|
+
async def run(self):
|
|
138
|
+
while not self.context.is_shutdown_requested():
|
|
139
|
+
self.counter += 1
|
|
140
|
+
await asyncio.sleep(1)
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
Any exception will cause pod crash
|
|
144
|
+
"""
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
147
|
+
# =========================================================================
|
|
148
|
+
# Optional Lifecycle Methods (Override if needed)
|
|
149
|
+
# =========================================================================
|
|
150
|
+
|
|
151
|
+
async def startup(self) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Initialize processor resources (optional).
|
|
154
|
+
|
|
155
|
+
Called once at pod startup after __init__ but before run().
|
|
156
|
+
Override to load models, open connections, etc.
|
|
157
|
+
|
|
158
|
+
Default: No-op
|
|
159
|
+
"""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
async def shutdown(self) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Cleanup processor resources (optional).
|
|
165
|
+
|
|
166
|
+
Called on graceful shutdown (SIGTERM). Has max timeout
|
|
167
|
+
(configurable via DORY_SHUTDOWN_TIMEOUT_SEC, default 30s).
|
|
168
|
+
Override to close connections, flush buffers, etc.
|
|
169
|
+
|
|
170
|
+
Default: No-op
|
|
171
|
+
"""
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def get_state(self) -> dict:
|
|
175
|
+
"""
|
|
176
|
+
Return state to migrate to next pod (optional).
|
|
177
|
+
|
|
178
|
+
Called during migration (must be fast, <1s). State must be
|
|
179
|
+
JSON-serializable.
|
|
180
|
+
|
|
181
|
+
Default: Returns all @stateful decorated attributes, or {} if none.
|
|
182
|
+
|
|
183
|
+
Override for custom state:
|
|
184
|
+
def get_state(self):
|
|
185
|
+
return {"counter": self.counter, "data": self.data}
|
|
186
|
+
"""
|
|
187
|
+
# Auto-collect @stateful decorated attributes
|
|
188
|
+
stateful_state = get_stateful_vars(self)
|
|
189
|
+
if stateful_state:
|
|
190
|
+
return stateful_state
|
|
191
|
+
return {}
|
|
192
|
+
|
|
193
|
+
async def restore_state(self, state: dict) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Restore state from previous pod (optional).
|
|
196
|
+
|
|
197
|
+
Called after startup() but before run() if state exists.
|
|
198
|
+
|
|
199
|
+
Default: Restores all @stateful decorated attributes from state.
|
|
200
|
+
|
|
201
|
+
Override for custom restoration:
|
|
202
|
+
async def restore_state(self, state):
|
|
203
|
+
self.counter = state.get("counter", 0)
|
|
204
|
+
"""
|
|
205
|
+
# Auto-restore @stateful decorated attributes
|
|
206
|
+
set_stateful_vars(self, state)
|
|
207
|
+
|
|
208
|
+
# =========================================================================
|
|
209
|
+
# Helper Methods
|
|
210
|
+
# =========================================================================
|
|
211
|
+
|
|
212
|
+
async def run_loop(
|
|
213
|
+
self,
|
|
214
|
+
interval: float = 1.0,
|
|
215
|
+
check_migration: bool = True,
|
|
216
|
+
) -> AsyncIterator[int]:
|
|
217
|
+
"""
|
|
218
|
+
Async iterator that yields until shutdown is requested.
|
|
219
|
+
|
|
220
|
+
Simplifies the common pattern of checking shutdown in a loop.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
interval: Sleep interval between iterations (seconds)
|
|
224
|
+
check_migration: If True, also yields when migration is imminent
|
|
225
|
+
|
|
226
|
+
Yields:
|
|
227
|
+
Iteration count (0, 1, 2, ...)
|
|
228
|
+
|
|
229
|
+
Usage:
|
|
230
|
+
async def run(self):
|
|
231
|
+
async for i in self.run_loop(interval=1):
|
|
232
|
+
self.counter += 1
|
|
233
|
+
print(f"Iteration {i}")
|
|
234
|
+
|
|
235
|
+
# Equivalent to:
|
|
236
|
+
async def run(self):
|
|
237
|
+
i = 0
|
|
238
|
+
while not self.context.is_shutdown_requested():
|
|
239
|
+
self.counter += 1
|
|
240
|
+
print(f"Iteration {i}")
|
|
241
|
+
i += 1
|
|
242
|
+
await asyncio.sleep(1)
|
|
243
|
+
"""
|
|
244
|
+
iteration = 0
|
|
245
|
+
while not self.context.is_shutdown_requested():
|
|
246
|
+
yield iteration
|
|
247
|
+
iteration += 1
|
|
248
|
+
|
|
249
|
+
# Check if migration is imminent
|
|
250
|
+
if check_migration and self.context.is_migration_imminent():
|
|
251
|
+
self.context.logger().info(
|
|
252
|
+
f"Migration imminent, completing iteration {iteration}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
await asyncio.sleep(interval)
|
|
256
|
+
|
|
257
|
+
def is_shutting_down(self) -> bool:
|
|
258
|
+
"""
|
|
259
|
+
Convenience method to check if shutdown is requested.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if shutdown has been requested
|
|
263
|
+
"""
|
|
264
|
+
return self.context.is_shutdown_requested()
|
|
265
|
+
|
|
266
|
+
# =========================================================================
|
|
267
|
+
# Optional Fault Handling Hooks
|
|
268
|
+
# =========================================================================
|
|
269
|
+
|
|
270
|
+
async def on_state_restore_failed(self, error: Exception) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Called if state restore fails.
|
|
273
|
+
|
|
274
|
+
Override to attempt recovery (e.g., fetch from external backup).
|
|
275
|
+
Return True to start with golden image, False to exit and crash.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
error: Exception from restore_state() or validation
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True to continue with golden image, False to exit
|
|
282
|
+
"""
|
|
283
|
+
return True # Default: continue with golden image
|
|
284
|
+
|
|
285
|
+
async def on_rapid_restart_detected(self, restart_count: int) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Called if restart loop detected (3+ restarts in 5 minutes).
|
|
288
|
+
|
|
289
|
+
Override to attempt recovery (e.g., reinitialize state, reset
|
|
290
|
+
connections). Return True to continue, False to trigger golden reset.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
restart_count: Number of restarts detected
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
True to continue, False to force golden reset
|
|
297
|
+
"""
|
|
298
|
+
return True # Default: continue (SDK will start golden)
|
|
299
|
+
|
|
300
|
+
async def on_health_check_failed(self, error: Exception) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Called if health check fails.
|
|
303
|
+
|
|
304
|
+
Override to attempt recovery (e.g., reconnect to external services).
|
|
305
|
+
Return True to retry health check, False to fail.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
error: Exception from health check
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
True to retry, False to fail
|
|
312
|
+
"""
|
|
313
|
+
return False # Default: fail health check
|
|
314
|
+
|
|
315
|
+
def reset_caches(self) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Called during golden image reset.
|
|
318
|
+
|
|
319
|
+
Override to clear any in-memory caches, buffers, or temporary
|
|
320
|
+
state that should not persist through a golden reset.
|
|
321
|
+
"""
|
|
322
|
+
pass # Default: no caches to reset
|
|
323
|
+
|
|
324
|
+
# =========================================================================
|
|
325
|
+
# Auto-Initialization (SDK v2.1)
|
|
326
|
+
# =========================================================================
|
|
327
|
+
|
|
328
|
+
def _auto_initialize_components(self) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Auto-initialize SDK components from configuration.
|
|
331
|
+
|
|
332
|
+
This method is called automatically during __init__ if context is available.
|
|
333
|
+
Components are only initialized if enabled in configuration.
|
|
334
|
+
|
|
335
|
+
Initialized components:
|
|
336
|
+
- error_classifier: Always available
|
|
337
|
+
- circuit_breakers: Dict of circuit breakers
|
|
338
|
+
- otel: OpenTelemetry (if enabled)
|
|
339
|
+
- request_tracker: Request tracking (if enabled)
|
|
340
|
+
- request_id_middleware: Request ID generation (if enabled)
|
|
341
|
+
- connection_tracker: Connection tracking (if enabled)
|
|
342
|
+
"""
|
|
343
|
+
if not hasattr(self, "context") or self.context is None:
|
|
344
|
+
logger.debug("Context not available, skipping auto-initialization")
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
config = self.context.config
|
|
348
|
+
|
|
349
|
+
# 1. Error Classifier (always available)
|
|
350
|
+
self._init_error_classifier()
|
|
351
|
+
|
|
352
|
+
# 2. Circuit Breakers (auto-created from config)
|
|
353
|
+
self._init_circuit_breakers(config)
|
|
354
|
+
|
|
355
|
+
# 3. OpenTelemetry (auto-initialized if enabled)
|
|
356
|
+
self._init_opentelemetry(config)
|
|
357
|
+
|
|
358
|
+
# 4. Request Tracking (auto-initialized if enabled)
|
|
359
|
+
self._init_request_tracking(config)
|
|
360
|
+
|
|
361
|
+
# 5. Request ID Middleware (auto-initialized if enabled)
|
|
362
|
+
self._init_request_id(config)
|
|
363
|
+
|
|
364
|
+
# 6. Connection Tracker (auto-initialized if enabled)
|
|
365
|
+
self._init_connection_tracking(config)
|
|
366
|
+
|
|
367
|
+
logger.debug("Auto-initialization complete")
|
|
368
|
+
|
|
369
|
+
def _init_error_classifier(self) -> None:
|
|
370
|
+
"""Initialize error classifier (always available)."""
|
|
371
|
+
try:
|
|
372
|
+
from dory.errors import ErrorClassifier
|
|
373
|
+
|
|
374
|
+
self.error_classifier = ErrorClassifier()
|
|
375
|
+
logger.debug("Initialized error classifier")
|
|
376
|
+
except ImportError:
|
|
377
|
+
logger.debug("Error classifier not available (dory.errors not installed)")
|
|
378
|
+
self.error_classifier = None
|
|
379
|
+
|
|
380
|
+
def _init_circuit_breakers(self, config: Any) -> None:
|
|
381
|
+
"""Auto-initialize circuit breakers from configuration."""
|
|
382
|
+
self.circuit_breakers: Dict[str, Any] = {}
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
from dory.resilience import CircuitBreaker
|
|
386
|
+
except ImportError:
|
|
387
|
+
logger.debug("Circuit breakers not available (dory.resilience not installed)")
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
# Get circuit breaker config
|
|
391
|
+
cb_config_dict = {}
|
|
392
|
+
if hasattr(config, "__dict__"):
|
|
393
|
+
config_dict = config.__dict__
|
|
394
|
+
elif hasattr(config, "model_dump"):
|
|
395
|
+
config_dict = config.model_dump()
|
|
396
|
+
else:
|
|
397
|
+
config_dict = {}
|
|
398
|
+
|
|
399
|
+
# Try to get from nested config structure
|
|
400
|
+
if "circuit_breaker" in config_dict:
|
|
401
|
+
cb_config_dict = config_dict["circuit_breaker"]
|
|
402
|
+
elif hasattr(config, "get"):
|
|
403
|
+
cb_config_dict = config.get("circuit_breaker", {})
|
|
404
|
+
|
|
405
|
+
# Check if circuit breakers are enabled
|
|
406
|
+
if isinstance(cb_config_dict, dict) and not cb_config_dict.get("enabled", True):
|
|
407
|
+
logger.info("Circuit breakers disabled in configuration")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
# Get default parameters
|
|
411
|
+
if isinstance(cb_config_dict, dict):
|
|
412
|
+
failure_threshold = cb_config_dict.get("failure_threshold", 5)
|
|
413
|
+
success_threshold = cb_config_dict.get("success_threshold", 2)
|
|
414
|
+
timeout_seconds = cb_config_dict.get("timeout", 30.0)
|
|
415
|
+
half_open_max_calls = cb_config_dict.get("half_open_max_calls", 3)
|
|
416
|
+
else:
|
|
417
|
+
failure_threshold = 5
|
|
418
|
+
success_threshold = 2
|
|
419
|
+
timeout_seconds = 30.0
|
|
420
|
+
half_open_max_calls = 3
|
|
421
|
+
|
|
422
|
+
# Create default circuit breakers for common services
|
|
423
|
+
common_names = ["database", "external_api", "cache"]
|
|
424
|
+
for name in common_names:
|
|
425
|
+
self.circuit_breakers[name] = CircuitBreaker(
|
|
426
|
+
name=name,
|
|
427
|
+
failure_threshold=failure_threshold,
|
|
428
|
+
success_threshold=success_threshold,
|
|
429
|
+
timeout_seconds=timeout_seconds,
|
|
430
|
+
half_open_max_calls=half_open_max_calls,
|
|
431
|
+
)
|
|
432
|
+
logger.debug(f"Created circuit breaker: {name}")
|
|
433
|
+
|
|
434
|
+
# Create custom circuit breakers from config
|
|
435
|
+
if isinstance(cb_config_dict, dict) and "breakers" in cb_config_dict:
|
|
436
|
+
custom_breakers = cb_config_dict["breakers"]
|
|
437
|
+
for name, breaker_config in custom_breakers.items():
|
|
438
|
+
self.circuit_breakers[name] = CircuitBreaker(
|
|
439
|
+
name=name,
|
|
440
|
+
failure_threshold=breaker_config.get("failure_threshold", failure_threshold),
|
|
441
|
+
success_threshold=breaker_config.get("success_threshold", success_threshold),
|
|
442
|
+
timeout_seconds=breaker_config.get("timeout", timeout_seconds),
|
|
443
|
+
half_open_max_calls=breaker_config.get("half_open_max_calls", half_open_max_calls),
|
|
444
|
+
)
|
|
445
|
+
logger.debug(f"Created custom circuit breaker: {name}")
|
|
446
|
+
|
|
447
|
+
logger.info(f"Initialized {len(self.circuit_breakers)} circuit breakers")
|
|
448
|
+
|
|
449
|
+
def _init_opentelemetry(self, config: Any) -> None:
|
|
450
|
+
"""Auto-initialize OpenTelemetry if enabled."""
|
|
451
|
+
self.otel: Optional[Any] = None
|
|
452
|
+
|
|
453
|
+
# Get config dict
|
|
454
|
+
if hasattr(config, "__dict__"):
|
|
455
|
+
config_dict = config.__dict__
|
|
456
|
+
elif hasattr(config, "model_dump"):
|
|
457
|
+
config_dict = config.model_dump()
|
|
458
|
+
else:
|
|
459
|
+
config_dict = {}
|
|
460
|
+
|
|
461
|
+
# Get OpenTelemetry config
|
|
462
|
+
otel_config = {}
|
|
463
|
+
if "opentelemetry" in config_dict:
|
|
464
|
+
otel_config = config_dict["opentelemetry"]
|
|
465
|
+
elif hasattr(config, "get"):
|
|
466
|
+
otel_config = config.get("opentelemetry", {})
|
|
467
|
+
|
|
468
|
+
# Check if OpenTelemetry is enabled
|
|
469
|
+
if isinstance(otel_config, dict) and not otel_config.get("enabled", True):
|
|
470
|
+
logger.info("OpenTelemetry disabled in configuration")
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
from dory.monitoring import OpenTelemetryManager
|
|
475
|
+
|
|
476
|
+
# Get app config for service name/version
|
|
477
|
+
app_config = {}
|
|
478
|
+
if "app" in config_dict:
|
|
479
|
+
app_config = config_dict["app"]
|
|
480
|
+
elif hasattr(config, "get"):
|
|
481
|
+
app_config = config.get("app", {})
|
|
482
|
+
|
|
483
|
+
# Initialize OpenTelemetry
|
|
484
|
+
if isinstance(otel_config, dict):
|
|
485
|
+
self.otel = OpenTelemetryManager(
|
|
486
|
+
service_name=otel_config.get("service_name", app_config.get("name", "dory-app")),
|
|
487
|
+
service_version=otel_config.get(
|
|
488
|
+
"service_version", app_config.get("version", "1.0.0")
|
|
489
|
+
),
|
|
490
|
+
environment=otel_config.get("environment", app_config.get("environment", "production")),
|
|
491
|
+
console_export=otel_config.get("otlp", {}).get("console_export", True),
|
|
492
|
+
otlp_endpoint=otel_config.get("otlp", {}).get("endpoint"),
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
self.otel = OpenTelemetryManager(
|
|
496
|
+
service_name=app_config.get("name", "dory-app"),
|
|
497
|
+
service_version=app_config.get("version", "1.0.0"),
|
|
498
|
+
environment=app_config.get("environment", "production"),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
self.otel.initialize()
|
|
502
|
+
logger.info("OpenTelemetry initialized")
|
|
503
|
+
|
|
504
|
+
except ImportError:
|
|
505
|
+
logger.debug(
|
|
506
|
+
"OpenTelemetry not available. Install with: pip install dory-sdk[tracing]"
|
|
507
|
+
)
|
|
508
|
+
self.otel = None
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logger.warning(f"Failed to initialize OpenTelemetry: {e}")
|
|
511
|
+
self.otel = None
|
|
512
|
+
|
|
513
|
+
def _init_request_tracking(self, config: Any) -> None:
|
|
514
|
+
"""Auto-initialize request tracking if enabled."""
|
|
515
|
+
self.request_tracker: Optional[Any] = None
|
|
516
|
+
|
|
517
|
+
# Get config dict
|
|
518
|
+
if hasattr(config, "__dict__"):
|
|
519
|
+
config_dict = config.__dict__
|
|
520
|
+
elif hasattr(config, "model_dump"):
|
|
521
|
+
config_dict = config.model_dump()
|
|
522
|
+
else:
|
|
523
|
+
config_dict = {}
|
|
524
|
+
|
|
525
|
+
# Get bookkeeping config
|
|
526
|
+
bookkeeping_config = {}
|
|
527
|
+
if "bookkeeping" in config_dict:
|
|
528
|
+
bookkeeping_config = config_dict["bookkeeping"]
|
|
529
|
+
elif hasattr(config, "get"):
|
|
530
|
+
bookkeeping_config = config.get("bookkeeping", {})
|
|
531
|
+
|
|
532
|
+
# Get request tracking config
|
|
533
|
+
if isinstance(bookkeeping_config, dict):
|
|
534
|
+
tracking_config = bookkeeping_config.get("request_tracking", {})
|
|
535
|
+
else:
|
|
536
|
+
tracking_config = {}
|
|
537
|
+
|
|
538
|
+
# Check if request tracking is enabled
|
|
539
|
+
if isinstance(tracking_config, dict) and not tracking_config.get("enabled", True):
|
|
540
|
+
logger.info("Request tracking disabled in configuration")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
from dory.middleware import RequestTracker
|
|
545
|
+
|
|
546
|
+
if isinstance(tracking_config, dict):
|
|
547
|
+
self.request_tracker = RequestTracker(
|
|
548
|
+
max_concurrent=tracking_config.get("max_concurrent", 100),
|
|
549
|
+
timeout=tracking_config.get("timeout", 30.0),
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
self.request_tracker = RequestTracker()
|
|
553
|
+
|
|
554
|
+
logger.info("Request tracking initialized")
|
|
555
|
+
|
|
556
|
+
except ImportError:
|
|
557
|
+
logger.debug("Request tracking not available (dory.middleware not installed)")
|
|
558
|
+
self.request_tracker = None
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.warning(f"Failed to initialize request tracking: {e}")
|
|
561
|
+
self.request_tracker = None
|
|
562
|
+
|
|
563
|
+
def _init_request_id(self, config: Any) -> None:
|
|
564
|
+
"""Auto-initialize request ID middleware if enabled."""
|
|
565
|
+
self.request_id_middleware: Optional[Any] = None
|
|
566
|
+
|
|
567
|
+
# Get config dict
|
|
568
|
+
if hasattr(config, "__dict__"):
|
|
569
|
+
config_dict = config.__dict__
|
|
570
|
+
elif hasattr(config, "model_dump"):
|
|
571
|
+
config_dict = config.model_dump()
|
|
572
|
+
else:
|
|
573
|
+
config_dict = {}
|
|
574
|
+
|
|
575
|
+
# Get bookkeeping config
|
|
576
|
+
bookkeeping_config = {}
|
|
577
|
+
if "bookkeeping" in config_dict:
|
|
578
|
+
bookkeeping_config = config_dict["bookkeeping"]
|
|
579
|
+
elif hasattr(config, "get"):
|
|
580
|
+
bookkeeping_config = config.get("bookkeeping", {})
|
|
581
|
+
|
|
582
|
+
# Get request ID config
|
|
583
|
+
if isinstance(bookkeeping_config, dict):
|
|
584
|
+
request_id_config = bookkeeping_config.get("request_id", {})
|
|
585
|
+
else:
|
|
586
|
+
request_id_config = {}
|
|
587
|
+
|
|
588
|
+
# Check if request ID is enabled
|
|
589
|
+
if isinstance(request_id_config, dict) and not request_id_config.get("enabled", True):
|
|
590
|
+
logger.info("Request ID generation disabled in configuration")
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
from dory.middleware import RequestIdMiddleware
|
|
595
|
+
|
|
596
|
+
if isinstance(request_id_config, dict):
|
|
597
|
+
self.request_id_middleware = RequestIdMiddleware(
|
|
598
|
+
format=request_id_config.get("format", "uuid4"),
|
|
599
|
+
add_to_response=request_id_config.get("add_to_response", True),
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
self.request_id_middleware = RequestIdMiddleware()
|
|
603
|
+
|
|
604
|
+
logger.info("Request ID middleware initialized")
|
|
605
|
+
|
|
606
|
+
except ImportError:
|
|
607
|
+
logger.debug("Request ID middleware not available (dory.middleware not installed)")
|
|
608
|
+
self.request_id_middleware = None
|
|
609
|
+
except Exception as e:
|
|
610
|
+
logger.warning(f"Failed to initialize request ID middleware: {e}")
|
|
611
|
+
self.request_id_middleware = None
|
|
612
|
+
|
|
613
|
+
def _init_connection_tracking(self, config: Any) -> None:
|
|
614
|
+
"""Auto-initialize connection tracking if enabled."""
|
|
615
|
+
self.connection_tracker: Optional[Any] = None
|
|
616
|
+
|
|
617
|
+
# Get config dict
|
|
618
|
+
if hasattr(config, "__dict__"):
|
|
619
|
+
config_dict = config.__dict__
|
|
620
|
+
elif hasattr(config, "model_dump"):
|
|
621
|
+
config_dict = config.model_dump()
|
|
622
|
+
else:
|
|
623
|
+
config_dict = {}
|
|
624
|
+
|
|
625
|
+
# Get bookkeeping config
|
|
626
|
+
bookkeeping_config = {}
|
|
627
|
+
if "bookkeeping" in config_dict:
|
|
628
|
+
bookkeeping_config = config_dict["bookkeeping"]
|
|
629
|
+
elif hasattr(config, "get"):
|
|
630
|
+
bookkeeping_config = config.get("bookkeeping", {})
|
|
631
|
+
|
|
632
|
+
# Get connection tracking config
|
|
633
|
+
if isinstance(bookkeeping_config, dict):
|
|
634
|
+
connection_config = bookkeeping_config.get("connection_tracking", {})
|
|
635
|
+
else:
|
|
636
|
+
connection_config = {}
|
|
637
|
+
|
|
638
|
+
# Check if connection tracking is enabled
|
|
639
|
+
if isinstance(connection_config, dict) and not connection_config.get("enabled", True):
|
|
640
|
+
logger.info("Connection tracking disabled in configuration")
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
from dory.middleware import ConnectionTracker
|
|
645
|
+
|
|
646
|
+
self.connection_tracker = ConnectionTracker()
|
|
647
|
+
logger.info("Connection tracking initialized")
|
|
648
|
+
|
|
649
|
+
except ImportError:
|
|
650
|
+
logger.debug("Connection tracking not available (dory.middleware not installed)")
|
|
651
|
+
self.connection_tracker = None
|
|
652
|
+
except Exception as e:
|
|
653
|
+
logger.warning(f"Failed to initialize connection tracking: {e}")
|
|
654
|
+
self.connection_tracker = None
|