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/config/loader.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loader for Dory SDK.
|
|
3
|
+
|
|
4
|
+
Supports loading from:
|
|
5
|
+
1. YAML configuration file
|
|
6
|
+
2. Environment variables (DORY_ prefix)
|
|
7
|
+
3. Configuration presets
|
|
8
|
+
4. Auto-detection (service name, version, environment)
|
|
9
|
+
5. Default values
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from dory.config.schema import DoryConfig
|
|
22
|
+
from dory.config.defaults import DEFAULT_CONFIG
|
|
23
|
+
from dory.config.presets import get_preset, list_presets, DEVELOPMENT_PRESET
|
|
24
|
+
from dory.utils.errors import DoryConfigError
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigLoader:
|
|
30
|
+
"""
|
|
31
|
+
Smart configuration loader with:
|
|
32
|
+
- Auto-detection of service name/version
|
|
33
|
+
- Preset support (development, production, high-availability)
|
|
34
|
+
- Zero-config mode
|
|
35
|
+
- Progressive disclosure
|
|
36
|
+
|
|
37
|
+
Priority order:
|
|
38
|
+
1. Environment variables (highest)
|
|
39
|
+
2. Config file
|
|
40
|
+
3. Preset (if specified)
|
|
41
|
+
4. Auto-detected defaults
|
|
42
|
+
5. Default values (lowest)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
ENV_PREFIX = "DORY_"
|
|
46
|
+
DEFAULT_CONFIG_PATHS = [
|
|
47
|
+
"/etc/dory/config.yaml",
|
|
48
|
+
"/app/config/dory.yaml",
|
|
49
|
+
"./dory.yaml",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def __init__(self, config_file: str | None = None):
|
|
53
|
+
"""
|
|
54
|
+
Initialize config loader.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config_file: Optional path to YAML config file
|
|
58
|
+
"""
|
|
59
|
+
self._config_file = config_file
|
|
60
|
+
|
|
61
|
+
def load(self) -> DoryConfig:
|
|
62
|
+
"""
|
|
63
|
+
Load and validate configuration with smart defaults.
|
|
64
|
+
|
|
65
|
+
Priority:
|
|
66
|
+
1. Environment variables (DORY_*)
|
|
67
|
+
2. Config file (dory.yaml)
|
|
68
|
+
3. Preset (if specified)
|
|
69
|
+
4. Auto-detected defaults
|
|
70
|
+
5. Default values
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Validated DoryConfig instance
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
DoryConfigError: If configuration is invalid
|
|
77
|
+
"""
|
|
78
|
+
# Try to load config file
|
|
79
|
+
file_config = self._load_from_file()
|
|
80
|
+
|
|
81
|
+
# Determine which preset to use
|
|
82
|
+
preset_name = self._determine_preset(file_config)
|
|
83
|
+
|
|
84
|
+
# Start with preset or development defaults
|
|
85
|
+
if preset_name in list_presets():
|
|
86
|
+
config_dict = get_preset(preset_name)
|
|
87
|
+
logger.info(f"Using configuration preset: {preset_name}")
|
|
88
|
+
else:
|
|
89
|
+
config_dict = DEVELOPMENT_PRESET.copy()
|
|
90
|
+
if preset_name:
|
|
91
|
+
logger.warning(f"Unknown preset '{preset_name}', using development defaults")
|
|
92
|
+
else:
|
|
93
|
+
logger.info("No preset specified, using development mode with auto-detection")
|
|
94
|
+
|
|
95
|
+
# Auto-detect application info
|
|
96
|
+
app_config = self._auto_detect_app_info(file_config.get("app", {}) if file_config else {})
|
|
97
|
+
config_dict["app"] = app_config
|
|
98
|
+
|
|
99
|
+
# Deep merge file config (overrides preset)
|
|
100
|
+
if file_config:
|
|
101
|
+
config_dict = self._deep_merge(config_dict, file_config)
|
|
102
|
+
|
|
103
|
+
# Apply environment variable overrides (highest priority)
|
|
104
|
+
env_config = self._load_from_env()
|
|
105
|
+
config_dict = self._deep_merge(config_dict, env_config)
|
|
106
|
+
|
|
107
|
+
# Validate and create config object
|
|
108
|
+
try:
|
|
109
|
+
config = DoryConfig(**config_dict)
|
|
110
|
+
logger.info(f"Configuration loaded: {app_config.get('name', 'unknown')} v{app_config.get('version', 'unknown')}")
|
|
111
|
+
logger.debug(f"Full configuration: {config.model_dump()}")
|
|
112
|
+
return config
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise DoryConfigError(f"Invalid configuration: {e}", cause=e)
|
|
115
|
+
|
|
116
|
+
def _load_from_file(self) -> dict[str, Any] | None:
|
|
117
|
+
"""Load configuration from YAML file."""
|
|
118
|
+
config_path = self._find_config_file()
|
|
119
|
+
|
|
120
|
+
if not config_path:
|
|
121
|
+
logger.debug("No config file found, using defaults")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
with open(config_path) as f:
|
|
126
|
+
config = yaml.safe_load(f) or {}
|
|
127
|
+
|
|
128
|
+
logger.info(f"Loaded config from {config_path}")
|
|
129
|
+
return config
|
|
130
|
+
|
|
131
|
+
except yaml.YAMLError as e:
|
|
132
|
+
raise DoryConfigError(f"Invalid YAML in {config_path}: {e}", cause=e)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise DoryConfigError(f"Failed to read {config_path}: {e}", cause=e)
|
|
135
|
+
|
|
136
|
+
def _find_config_file(self) -> Path | None:
|
|
137
|
+
"""Find config file from explicit path or default locations."""
|
|
138
|
+
# Check explicit path first
|
|
139
|
+
if self._config_file:
|
|
140
|
+
path = Path(self._config_file)
|
|
141
|
+
if path.exists():
|
|
142
|
+
return path
|
|
143
|
+
raise DoryConfigError(f"Config file not found: {self._config_file}")
|
|
144
|
+
|
|
145
|
+
# Check environment variable
|
|
146
|
+
env_path = os.environ.get("DORY_CONFIG_FILE")
|
|
147
|
+
if env_path:
|
|
148
|
+
path = Path(env_path)
|
|
149
|
+
if path.exists():
|
|
150
|
+
return path
|
|
151
|
+
logger.warning(f"DORY_CONFIG_FILE set but not found: {env_path}")
|
|
152
|
+
|
|
153
|
+
# Check default locations
|
|
154
|
+
for default_path in self.DEFAULT_CONFIG_PATHS:
|
|
155
|
+
path = Path(default_path)
|
|
156
|
+
if path.exists():
|
|
157
|
+
return path
|
|
158
|
+
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _load_from_env(self) -> dict[str, Any]:
|
|
162
|
+
"""Load configuration from environment variables."""
|
|
163
|
+
config = {}
|
|
164
|
+
|
|
165
|
+
# Map of config keys to environment variable names
|
|
166
|
+
env_mapping = {
|
|
167
|
+
"startup_timeout_sec": "DORY_STARTUP_TIMEOUT_SEC",
|
|
168
|
+
"shutdown_timeout_sec": "DORY_SHUTDOWN_TIMEOUT_SEC",
|
|
169
|
+
"health_check_interval_sec": "DORY_HEALTH_CHECK_INTERVAL_SEC",
|
|
170
|
+
"health_port": "DORY_HEALTH_PORT",
|
|
171
|
+
"health_path": "DORY_HEALTH_PATH",
|
|
172
|
+
"ready_path": "DORY_READY_PATH",
|
|
173
|
+
"metrics_path": "DORY_METRICS_PATH",
|
|
174
|
+
"state_backend": "DORY_STATE_BACKEND",
|
|
175
|
+
"state_pvc_mount": "DORY_STATE_PVC_MOUNT",
|
|
176
|
+
"state_s3_bucket": "DORY_STATE_S3_BUCKET",
|
|
177
|
+
"state_s3_prefix": "DORY_STATE_S3_PREFIX",
|
|
178
|
+
"max_restart_attempts": "DORY_MAX_RESTART_ATTEMPTS",
|
|
179
|
+
"restart_backoff_sec": "DORY_RESTART_BACKOFF_SEC",
|
|
180
|
+
"golden_image_threshold": "DORY_GOLDEN_IMAGE_THRESHOLD",
|
|
181
|
+
"log_level": "DORY_LOG_LEVEL",
|
|
182
|
+
"log_format": "DORY_LOG_FORMAT",
|
|
183
|
+
"metrics_enabled": "DORY_METRICS_ENABLED",
|
|
184
|
+
"metrics_prefix": "DORY_METRICS_PREFIX",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for config_key, env_var in env_mapping.items():
|
|
188
|
+
value = os.environ.get(env_var)
|
|
189
|
+
if value is not None:
|
|
190
|
+
# Convert to appropriate type
|
|
191
|
+
config[config_key] = self._convert_env_value(config_key, value)
|
|
192
|
+
|
|
193
|
+
return config
|
|
194
|
+
|
|
195
|
+
def _convert_env_value(self, key: str, value: str) -> Any:
|
|
196
|
+
"""Convert environment variable string to appropriate type."""
|
|
197
|
+
# Integer fields
|
|
198
|
+
int_fields = {
|
|
199
|
+
"startup_timeout_sec",
|
|
200
|
+
"shutdown_timeout_sec",
|
|
201
|
+
"health_check_interval_sec",
|
|
202
|
+
"health_port",
|
|
203
|
+
"max_restart_attempts",
|
|
204
|
+
"restart_backoff_sec",
|
|
205
|
+
"golden_image_threshold",
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Boolean fields
|
|
209
|
+
bool_fields = {"metrics_enabled"}
|
|
210
|
+
|
|
211
|
+
if key in int_fields:
|
|
212
|
+
try:
|
|
213
|
+
return int(value)
|
|
214
|
+
except ValueError:
|
|
215
|
+
raise DoryConfigError(f"Invalid integer for {key}: {value}")
|
|
216
|
+
|
|
217
|
+
if key in bool_fields:
|
|
218
|
+
return value.lower() in ("true", "1", "yes", "on")
|
|
219
|
+
|
|
220
|
+
return value
|
|
221
|
+
|
|
222
|
+
def _determine_preset(self, file_config: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
223
|
+
"""
|
|
224
|
+
Determine which preset to use.
|
|
225
|
+
|
|
226
|
+
Priority:
|
|
227
|
+
1. DORY_PRESET environment variable
|
|
228
|
+
2. preset field in config file
|
|
229
|
+
3. None (uses development preset)
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
file_config: Configuration loaded from file
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Preset name or None
|
|
236
|
+
"""
|
|
237
|
+
# Check environment variable first
|
|
238
|
+
preset = os.environ.get("DORY_PRESET")
|
|
239
|
+
if preset:
|
|
240
|
+
logger.debug(f"Preset from environment: {preset}")
|
|
241
|
+
return preset
|
|
242
|
+
|
|
243
|
+
# Check config file
|
|
244
|
+
if file_config and "preset" in file_config:
|
|
245
|
+
preset = file_config["preset"]
|
|
246
|
+
logger.debug(f"Preset from config file: {preset}")
|
|
247
|
+
return preset
|
|
248
|
+
|
|
249
|
+
# No preset specified
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
def _auto_detect_app_info(self, user_app: Dict[str, Any]) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Auto-detect service name, version, and environment.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
user_app: User-provided app configuration
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Dictionary with app configuration
|
|
261
|
+
"""
|
|
262
|
+
app = {}
|
|
263
|
+
|
|
264
|
+
# Auto-detect service name
|
|
265
|
+
if "name" in user_app:
|
|
266
|
+
app["name"] = user_app["name"]
|
|
267
|
+
else:
|
|
268
|
+
# Try to detect from directory name
|
|
269
|
+
cwd = Path.cwd()
|
|
270
|
+
app["name"] = cwd.name
|
|
271
|
+
logger.debug(f"Auto-detected service name: {app['name']}")
|
|
272
|
+
|
|
273
|
+
# Auto-detect version
|
|
274
|
+
if "version" in user_app:
|
|
275
|
+
app["version"] = user_app["version"]
|
|
276
|
+
else:
|
|
277
|
+
# Try to detect from git tag, pyproject.toml, or use default
|
|
278
|
+
app["version"] = self._detect_version()
|
|
279
|
+
logger.debug(f"Auto-detected version: {app['version']}")
|
|
280
|
+
|
|
281
|
+
# Auto-detect environment
|
|
282
|
+
if "environment" in user_app:
|
|
283
|
+
app["environment"] = user_app["environment"]
|
|
284
|
+
else:
|
|
285
|
+
app["environment"] = os.environ.get("ENVIRONMENT", "development")
|
|
286
|
+
logger.debug(f"Auto-detected environment: {app['environment']}")
|
|
287
|
+
|
|
288
|
+
# Copy any other user-provided app config
|
|
289
|
+
for key, value in user_app.items():
|
|
290
|
+
if key not in app:
|
|
291
|
+
app[key] = value
|
|
292
|
+
|
|
293
|
+
return app
|
|
294
|
+
|
|
295
|
+
def _detect_version(self) -> str:
|
|
296
|
+
"""
|
|
297
|
+
Try to detect version from various sources.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Version string
|
|
301
|
+
"""
|
|
302
|
+
# Try git tag
|
|
303
|
+
try:
|
|
304
|
+
result = subprocess.run(
|
|
305
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
306
|
+
capture_output=True,
|
|
307
|
+
text=True,
|
|
308
|
+
timeout=1,
|
|
309
|
+
check=False,
|
|
310
|
+
)
|
|
311
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
312
|
+
return result.stdout.strip()
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
# Try pyproject.toml
|
|
317
|
+
pyproject_path = Path("pyproject.toml")
|
|
318
|
+
if pyproject_path.exists():
|
|
319
|
+
try:
|
|
320
|
+
with open(pyproject_path) as f:
|
|
321
|
+
content = f.read()
|
|
322
|
+
# Simple regex to extract version
|
|
323
|
+
match = re.search(r'version\s*=\s*"([^"]+)"', content)
|
|
324
|
+
if match:
|
|
325
|
+
return match.group(1)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
# Try package.json (for Node.js projects)
|
|
330
|
+
package_json_path = Path("package.json")
|
|
331
|
+
if package_json_path.exists():
|
|
332
|
+
try:
|
|
333
|
+
with open(package_json_path) as f:
|
|
334
|
+
import json
|
|
335
|
+
data = json.load(f)
|
|
336
|
+
if "version" in data:
|
|
337
|
+
return data["version"]
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
|
|
341
|
+
# Default
|
|
342
|
+
return "0.1.0-dev"
|
|
343
|
+
|
|
344
|
+
def _deep_merge(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
345
|
+
"""
|
|
346
|
+
Deep merge two dictionaries.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
base: Base dictionary
|
|
350
|
+
override: Dictionary with override values
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Merged dictionary
|
|
354
|
+
"""
|
|
355
|
+
result = base.copy()
|
|
356
|
+
for key, value in override.items():
|
|
357
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
358
|
+
result[key] = self._deep_merge(result[key], value)
|
|
359
|
+
else:
|
|
360
|
+
result[key] = value
|
|
361
|
+
return result
|
dory/config/presets.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Configuration presets for common scenarios."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
# Development preset - developer-friendly defaults
|
|
6
|
+
DEVELOPMENT_PRESET = {
|
|
7
|
+
"environment": "development",
|
|
8
|
+
"log_level": "DEBUG",
|
|
9
|
+
"log_format": "text",
|
|
10
|
+
|
|
11
|
+
# Health server
|
|
12
|
+
"health_port": 8080,
|
|
13
|
+
"health_path": "/healthz",
|
|
14
|
+
"ready_path": "/ready",
|
|
15
|
+
"metrics_path": "/metrics",
|
|
16
|
+
|
|
17
|
+
# State management
|
|
18
|
+
"state_backend": "local",
|
|
19
|
+
"state_pvc_mount": "/data",
|
|
20
|
+
|
|
21
|
+
# Lifecycle timeouts
|
|
22
|
+
"startup_timeout_sec": 30,
|
|
23
|
+
"shutdown_timeout_sec": 30,
|
|
24
|
+
"health_check_interval_sec": 10,
|
|
25
|
+
|
|
26
|
+
# Recovery
|
|
27
|
+
"max_restart_attempts": 3,
|
|
28
|
+
"restart_backoff_sec": 10,
|
|
29
|
+
"golden_image_threshold": 5,
|
|
30
|
+
|
|
31
|
+
# Metrics
|
|
32
|
+
"metrics_enabled": True,
|
|
33
|
+
"metrics_prefix": "dory",
|
|
34
|
+
|
|
35
|
+
# Retry configuration
|
|
36
|
+
"retry": {
|
|
37
|
+
"max_attempts": 3,
|
|
38
|
+
"initial_delay": 0.1,
|
|
39
|
+
"multiplier": 2.0,
|
|
40
|
+
"max_delay": 30.0,
|
|
41
|
+
"jitter": True,
|
|
42
|
+
"budget_percent": 20.0,
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
# Circuit breaker configuration
|
|
46
|
+
"circuit_breaker": {
|
|
47
|
+
"failure_threshold": 5,
|
|
48
|
+
"success_threshold": 2,
|
|
49
|
+
"timeout": 30.0,
|
|
50
|
+
"half_open_max_calls": 3,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
# Error classification
|
|
54
|
+
"error_classification": {
|
|
55
|
+
"enabled": True,
|
|
56
|
+
"unknown_as_transient": False,
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
# OpenTelemetry configuration
|
|
60
|
+
"opentelemetry": {
|
|
61
|
+
"enabled": True,
|
|
62
|
+
"console_export": True,
|
|
63
|
+
"sampling": {"ratio": 1.0},
|
|
64
|
+
"otlp": {
|
|
65
|
+
"endpoint": "",
|
|
66
|
+
"console_export": True,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
# Bookkeeping configuration
|
|
71
|
+
"bookkeeping": {
|
|
72
|
+
"request_tracking": {
|
|
73
|
+
"enabled": True,
|
|
74
|
+
"max_concurrent": 100,
|
|
75
|
+
"timeout": 30.0,
|
|
76
|
+
"collect_metrics": True,
|
|
77
|
+
},
|
|
78
|
+
"request_id": {
|
|
79
|
+
"enabled": True,
|
|
80
|
+
"format": "uuid4",
|
|
81
|
+
"add_to_response": True,
|
|
82
|
+
},
|
|
83
|
+
"connection_tracking": {
|
|
84
|
+
"enabled": True,
|
|
85
|
+
"collect_metrics": True,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
# Middleware configuration
|
|
90
|
+
"middleware": {
|
|
91
|
+
"enabled": True,
|
|
92
|
+
"order": [
|
|
93
|
+
"request_id",
|
|
94
|
+
"request_tracker",
|
|
95
|
+
"opentelemetry",
|
|
96
|
+
"connection_tracker",
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Production preset - production-ready defaults
|
|
102
|
+
PRODUCTION_PRESET = {
|
|
103
|
+
"environment": "production",
|
|
104
|
+
"log_level": "INFO",
|
|
105
|
+
"log_format": "json",
|
|
106
|
+
|
|
107
|
+
# Health server
|
|
108
|
+
"health_port": 8080,
|
|
109
|
+
"health_path": "/healthz",
|
|
110
|
+
"ready_path": "/ready",
|
|
111
|
+
"metrics_path": "/metrics",
|
|
112
|
+
|
|
113
|
+
# State management
|
|
114
|
+
"state_backend": "configmap",
|
|
115
|
+
"state_pvc_mount": "/data",
|
|
116
|
+
|
|
117
|
+
# Lifecycle timeouts
|
|
118
|
+
"startup_timeout_sec": 30,
|
|
119
|
+
"shutdown_timeout_sec": 30,
|
|
120
|
+
"health_check_interval_sec": 10,
|
|
121
|
+
|
|
122
|
+
# Recovery
|
|
123
|
+
"max_restart_attempts": 3,
|
|
124
|
+
"restart_backoff_sec": 10,
|
|
125
|
+
"golden_image_threshold": 5,
|
|
126
|
+
|
|
127
|
+
# Metrics
|
|
128
|
+
"metrics_enabled": True,
|
|
129
|
+
"metrics_prefix": "dory",
|
|
130
|
+
|
|
131
|
+
# Retry configuration
|
|
132
|
+
"retry": {
|
|
133
|
+
"max_attempts": 3,
|
|
134
|
+
"initial_delay": 0.1,
|
|
135
|
+
"multiplier": 2.0,
|
|
136
|
+
"max_delay": 30.0,
|
|
137
|
+
"jitter": True,
|
|
138
|
+
"budget_percent": 20.0,
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
# Circuit breaker configuration
|
|
142
|
+
"circuit_breaker": {
|
|
143
|
+
"failure_threshold": 5,
|
|
144
|
+
"success_threshold": 2,
|
|
145
|
+
"timeout": 30.0,
|
|
146
|
+
"half_open_max_calls": 3,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
# Error classification
|
|
150
|
+
"error_classification": {
|
|
151
|
+
"enabled": True,
|
|
152
|
+
"unknown_as_transient": False,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
# OpenTelemetry configuration
|
|
156
|
+
"opentelemetry": {
|
|
157
|
+
"enabled": True,
|
|
158
|
+
"console_export": False,
|
|
159
|
+
"sampling": {"ratio": 0.1}, # Sample 10% in production
|
|
160
|
+
"otlp": {
|
|
161
|
+
"endpoint": "",
|
|
162
|
+
"console_export": False,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
# Bookkeeping configuration
|
|
167
|
+
"bookkeeping": {
|
|
168
|
+
"request_tracking": {
|
|
169
|
+
"enabled": True,
|
|
170
|
+
"max_concurrent": 1000,
|
|
171
|
+
"timeout": 30.0,
|
|
172
|
+
"collect_metrics": True,
|
|
173
|
+
},
|
|
174
|
+
"request_id": {
|
|
175
|
+
"enabled": True,
|
|
176
|
+
"format": "uuid4",
|
|
177
|
+
"add_to_response": True,
|
|
178
|
+
},
|
|
179
|
+
"connection_tracking": {
|
|
180
|
+
"enabled": True,
|
|
181
|
+
"collect_metrics": True,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
# Middleware configuration
|
|
186
|
+
"middleware": {
|
|
187
|
+
"enabled": True,
|
|
188
|
+
"order": [
|
|
189
|
+
"request_id",
|
|
190
|
+
"request_tracker",
|
|
191
|
+
"opentelemetry",
|
|
192
|
+
"connection_tracker",
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# High-availability preset - aggressive fault tolerance
|
|
198
|
+
HIGH_AVAILABILITY_PRESET = {
|
|
199
|
+
"environment": "production",
|
|
200
|
+
"log_level": "INFO",
|
|
201
|
+
"log_format": "json",
|
|
202
|
+
|
|
203
|
+
# Health server
|
|
204
|
+
"health_port": 8080,
|
|
205
|
+
"health_path": "/healthz",
|
|
206
|
+
"ready_path": "/ready",
|
|
207
|
+
"metrics_path": "/metrics",
|
|
208
|
+
|
|
209
|
+
# State management
|
|
210
|
+
"state_backend": "pvc", # More reliable than configmap
|
|
211
|
+
"state_pvc_mount": "/data",
|
|
212
|
+
|
|
213
|
+
# Lifecycle timeouts
|
|
214
|
+
"startup_timeout_sec": 60, # More time for startup
|
|
215
|
+
"shutdown_timeout_sec": 60, # More time for graceful shutdown
|
|
216
|
+
"health_check_interval_sec": 5, # More frequent checks
|
|
217
|
+
|
|
218
|
+
# Recovery
|
|
219
|
+
"max_restart_attempts": 5, # More restart attempts
|
|
220
|
+
"restart_backoff_sec": 15, # Longer backoff
|
|
221
|
+
"golden_image_threshold": 10, # Higher threshold
|
|
222
|
+
|
|
223
|
+
# Metrics
|
|
224
|
+
"metrics_enabled": True,
|
|
225
|
+
"metrics_prefix": "dory",
|
|
226
|
+
|
|
227
|
+
# Retry configuration - more aggressive
|
|
228
|
+
"retry": {
|
|
229
|
+
"max_attempts": 5, # More retries
|
|
230
|
+
"initial_delay": 0.05,
|
|
231
|
+
"multiplier": 1.5,
|
|
232
|
+
"max_delay": 60.0,
|
|
233
|
+
"jitter": True,
|
|
234
|
+
"budget_percent": 30.0, # Higher budget
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
# Circuit breaker configuration - more sensitive
|
|
238
|
+
"circuit_breaker": {
|
|
239
|
+
"failure_threshold": 3, # Trip faster
|
|
240
|
+
"success_threshold": 5, # Need more successes to close
|
|
241
|
+
"timeout": 60.0, # Longer timeout
|
|
242
|
+
"half_open_max_calls": 5,
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
# Error classification
|
|
246
|
+
"error_classification": {
|
|
247
|
+
"enabled": True,
|
|
248
|
+
"unknown_as_transient": True, # Retry unknown errors
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
# OpenTelemetry configuration
|
|
252
|
+
"opentelemetry": {
|
|
253
|
+
"enabled": True,
|
|
254
|
+
"console_export": False,
|
|
255
|
+
"sampling": {"ratio": 1.0}, # Full sampling for debugging
|
|
256
|
+
"otlp": {
|
|
257
|
+
"endpoint": "",
|
|
258
|
+
"console_export": False,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
# Bookkeeping configuration
|
|
263
|
+
"bookkeeping": {
|
|
264
|
+
"request_tracking": {
|
|
265
|
+
"enabled": True,
|
|
266
|
+
"max_concurrent": 2000,
|
|
267
|
+
"timeout": 60.0,
|
|
268
|
+
"collect_metrics": True,
|
|
269
|
+
},
|
|
270
|
+
"request_id": {
|
|
271
|
+
"enabled": True,
|
|
272
|
+
"format": "uuid4",
|
|
273
|
+
"add_to_response": True,
|
|
274
|
+
},
|
|
275
|
+
"connection_tracking": {
|
|
276
|
+
"enabled": True,
|
|
277
|
+
"collect_metrics": True,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
# Middleware configuration
|
|
282
|
+
"middleware": {
|
|
283
|
+
"enabled": True,
|
|
284
|
+
"order": [
|
|
285
|
+
"request_id",
|
|
286
|
+
"request_tracker",
|
|
287
|
+
"opentelemetry",
|
|
288
|
+
"connection_tracker",
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
PRESETS: Dict[str, Dict[str, Any]] = {
|
|
294
|
+
"development": DEVELOPMENT_PRESET,
|
|
295
|
+
"production": PRODUCTION_PRESET,
|
|
296
|
+
"high-availability": HIGH_AVAILABILITY_PRESET,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_preset(name: str) -> Dict[str, Any]:
|
|
301
|
+
"""
|
|
302
|
+
Get configuration preset by name.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
name: Preset name (development, production, high-availability)
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Dictionary with preset configuration
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
ValueError: If preset name is unknown
|
|
312
|
+
"""
|
|
313
|
+
if name not in PRESETS:
|
|
314
|
+
raise ValueError(f"Unknown preset: {name}. Available: {list(PRESETS.keys())}")
|
|
315
|
+
return PRESETS[name].copy()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def list_presets() -> list[str]:
|
|
319
|
+
"""
|
|
320
|
+
Get list of available preset names.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
List of preset names
|
|
324
|
+
"""
|
|
325
|
+
return list(PRESETS.keys())
|