aegis-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aegis-stack might be problematic. Click here for more details.
- aegis/__init__.py +5 -0
- aegis/__main__.py +374 -0
- aegis/core/CLAUDE.md +365 -0
- aegis/core/__init__.py +6 -0
- aegis/core/components.py +115 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/template_generator.py +163 -0
- aegis/templates/CLAUDE.md +306 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis_stack-0.1.0.dist-info/METADATA +114 -0
- aegis_stack-0.1.0.dist-info/RECORD +103 -0
- aegis_stack-0.1.0.dist-info/WHEEL +4 -0
- aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for load test data structures.
|
|
3
|
+
|
|
4
|
+
Provides type safety and validation for load test configurations,
|
|
5
|
+
results, and analysis data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from app.components.worker.constants import LoadTestTypes
|
|
13
|
+
from app.core.config import get_load_test_queue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoadTestError(Exception):
|
|
17
|
+
"""Custom exception for load test operations."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LoadTestConfiguration(BaseModel):
|
|
22
|
+
"""Load test configuration with validation and defaults."""
|
|
23
|
+
|
|
24
|
+
num_tasks: int = Field(
|
|
25
|
+
default=100, ge=10, le=10000, description="Number of tasks to spawn"
|
|
26
|
+
)
|
|
27
|
+
task_type: LoadTestTypes = Field(
|
|
28
|
+
default=LoadTestTypes.CPU_INTENSIVE, description="Type of load test to run"
|
|
29
|
+
)
|
|
30
|
+
batch_size: int = Field(default=10, ge=1, le=100, description="Tasks per batch")
|
|
31
|
+
delay_ms: int = Field(
|
|
32
|
+
default=0, ge=0, le=5000, description="Delay between batches (ms)"
|
|
33
|
+
)
|
|
34
|
+
target_queue: str | None = Field(
|
|
35
|
+
default=None, description="Target queue for testing"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@field_validator('target_queue')
|
|
39
|
+
@classmethod
|
|
40
|
+
def set_default_queue(cls, v: str | None) -> str:
|
|
41
|
+
"""Set default queue if not specified."""
|
|
42
|
+
return v if v is not None else get_load_test_queue()
|
|
43
|
+
|
|
44
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
45
|
+
"""Convert configuration to dictionary for task enqueueing."""
|
|
46
|
+
data = super().model_dump(**kwargs)
|
|
47
|
+
# Convert enum to string value for task enqueueing
|
|
48
|
+
data["task_type"] = self.task_type.value
|
|
49
|
+
return data
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LoadTestMetrics(BaseModel):
|
|
53
|
+
"""Metrics from load test execution."""
|
|
54
|
+
|
|
55
|
+
tasks_sent: int = Field(..., ge=0, description="Total tasks enqueued")
|
|
56
|
+
tasks_completed: int = Field(..., ge=0, description="Successfully completed tasks")
|
|
57
|
+
tasks_failed: int = Field(0, ge=0, description="Failed tasks")
|
|
58
|
+
total_duration_seconds: float = Field(..., ge=0, description="Total test duration")
|
|
59
|
+
overall_throughput: float = Field(
|
|
60
|
+
0, ge=0, description="Overall throughput (tasks/sec)"
|
|
61
|
+
)
|
|
62
|
+
failure_rate_percent: float = Field(
|
|
63
|
+
0, ge=0, le=100, description="Failure rate percentage"
|
|
64
|
+
)
|
|
65
|
+
completion_percentage: float = Field(
|
|
66
|
+
0, ge=0, le=100, description="Completion percentage"
|
|
67
|
+
)
|
|
68
|
+
average_throughput_per_second: float = Field(
|
|
69
|
+
0, ge=0, description="Average throughput"
|
|
70
|
+
)
|
|
71
|
+
monitor_duration_seconds: float = Field(0, ge=0, description="Monitoring duration")
|
|
72
|
+
|
|
73
|
+
@field_validator("tasks_completed")
|
|
74
|
+
@classmethod
|
|
75
|
+
def completed_not_exceed_sent(cls, v: int, info: ValidationInfo) -> int:
|
|
76
|
+
"""Ensure completed tasks don't exceed sent tasks."""
|
|
77
|
+
if info.data and "tasks_sent" in info.data and v > info.data["tasks_sent"]:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Completed tasks ({v}) cannot exceed sent tasks "
|
|
80
|
+
f"({info.data['tasks_sent']})"
|
|
81
|
+
)
|
|
82
|
+
return v
|
|
83
|
+
|
|
84
|
+
@field_validator("tasks_failed")
|
|
85
|
+
@classmethod
|
|
86
|
+
def failed_not_exceed_sent(cls, v: int, info: ValidationInfo) -> int:
|
|
87
|
+
"""Ensure failed tasks don't exceed sent tasks."""
|
|
88
|
+
if info.data and "tasks_sent" in info.data and v > info.data["tasks_sent"]:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Failed tasks ({v}) cannot exceed sent tasks "
|
|
91
|
+
f"({info.data['tasks_sent']})"
|
|
92
|
+
)
|
|
93
|
+
return v
|
|
94
|
+
|
|
95
|
+
@field_validator("failure_rate_percent")
|
|
96
|
+
@classmethod
|
|
97
|
+
def validate_failure_rate_consistency(cls, v: float, info: ValidationInfo) -> float:
|
|
98
|
+
"""Ensure failure rate percentage matches task counts."""
|
|
99
|
+
if info.data and "tasks_sent" in info.data and "tasks_failed" in info.data:
|
|
100
|
+
tasks_sent = info.data["tasks_sent"]
|
|
101
|
+
tasks_failed = info.data["tasks_failed"]
|
|
102
|
+
if tasks_sent > 0:
|
|
103
|
+
calculated_rate = (tasks_failed / tasks_sent) * 100
|
|
104
|
+
# Allow small floating point differences (within 0.1%)
|
|
105
|
+
if abs(v - calculated_rate) > 0.1:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Failure rate {v}% doesn't match task counts "
|
|
108
|
+
f"({tasks_failed}/{tasks_sent} = {calculated_rate:.1f}%)"
|
|
109
|
+
)
|
|
110
|
+
return v
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PerformanceAnalysis(BaseModel):
|
|
114
|
+
"""Performance analysis results."""
|
|
115
|
+
|
|
116
|
+
throughput_rating: str = Field(
|
|
117
|
+
...,
|
|
118
|
+
pattern=r"^(unknown|poor|fair|good|excellent)$",
|
|
119
|
+
description="Throughput performance rating"
|
|
120
|
+
)
|
|
121
|
+
efficiency_rating: str = Field(
|
|
122
|
+
...,
|
|
123
|
+
pattern=r"^(unknown|poor|fair|good|excellent)$",
|
|
124
|
+
description="Task completion efficiency"
|
|
125
|
+
)
|
|
126
|
+
queue_pressure: str = Field(
|
|
127
|
+
...,
|
|
128
|
+
pattern=r"^(unknown|low|moderate|high)$",
|
|
129
|
+
description="Queue saturation level"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ValidationStatus(BaseModel):
|
|
134
|
+
"""Test execution validation status."""
|
|
135
|
+
|
|
136
|
+
test_type_verified: bool = Field(
|
|
137
|
+
default=False, description="Test type executed correctly"
|
|
138
|
+
)
|
|
139
|
+
expected_metrics_present: bool = Field(
|
|
140
|
+
default=False, description="Expected metrics are present"
|
|
141
|
+
)
|
|
142
|
+
performance_signature_match: str = Field(
|
|
143
|
+
default="unknown",
|
|
144
|
+
pattern=r"^(unknown|verified|partial|failed)$",
|
|
145
|
+
description="Performance matches expected patterns"
|
|
146
|
+
)
|
|
147
|
+
issues: list[str] = Field(default_factory=list, description="Validation issues")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestTypeInfo(BaseModel):
|
|
151
|
+
"""Information about a specific test type."""
|
|
152
|
+
|
|
153
|
+
name: str = Field(..., description="Human-readable test name")
|
|
154
|
+
description: str = Field(..., description="Test description")
|
|
155
|
+
expected_metrics: list[str] = Field(..., description="Expected result metrics")
|
|
156
|
+
performance_signature: str = Field(..., description="Expected performance pattern")
|
|
157
|
+
typical_duration_ms: str = Field(..., description="Typical execution time")
|
|
158
|
+
concurrency_impact: str = Field(..., description="Concurrency characteristics")
|
|
159
|
+
validation_keys: list[str] = Field(..., description="Keys for result validation")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class LoadTestAnalysis(BaseModel):
|
|
163
|
+
"""Complete load test analysis."""
|
|
164
|
+
|
|
165
|
+
test_type_info: TestTypeInfo = Field(..., description="Test type information")
|
|
166
|
+
performance_analysis: PerformanceAnalysis = Field(
|
|
167
|
+
..., description="Performance analysis"
|
|
168
|
+
)
|
|
169
|
+
validation_status: ValidationStatus = Field(..., description="Validation results")
|
|
170
|
+
recommendations: list[str] = Field(..., description="Improvement recommendations")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class LoadTestResult(BaseModel):
|
|
174
|
+
"""Complete load test result with analysis."""
|
|
175
|
+
|
|
176
|
+
task: str = Field(default="load_test_orchestrator", description="Task name")
|
|
177
|
+
status: str = Field(
|
|
178
|
+
...,
|
|
179
|
+
pattern=r"^(completed|failed|timed_out)$",
|
|
180
|
+
description="Test execution status"
|
|
181
|
+
)
|
|
182
|
+
test_id: str = Field(..., description="Unique test identifier")
|
|
183
|
+
configuration: LoadTestConfiguration = Field(..., description="Test configuration")
|
|
184
|
+
metrics: LoadTestMetrics = Field(..., description="Execution metrics")
|
|
185
|
+
start_time: str | None = Field(None, description="Test start time")
|
|
186
|
+
end_time: str | None = Field(None, description="Test end time")
|
|
187
|
+
task_ids: list[str] = Field(default_factory=list, description="Individual task IDs")
|
|
188
|
+
error: str | None = Field(None, description="Error message if failed")
|
|
189
|
+
analysis: LoadTestAnalysis | None = Field(
|
|
190
|
+
None, description="Performance analysis"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@model_validator(mode='after')
|
|
194
|
+
def validate_status_consistency(self) -> 'LoadTestResult':
|
|
195
|
+
"""Validate status consistency with error field."""
|
|
196
|
+
if self.status == 'failed' and not self.error:
|
|
197
|
+
raise ValueError("Failed status requires error message")
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class OrchestratorRawResult(BaseModel):
|
|
202
|
+
"""Raw orchestrator result format for transformation."""
|
|
203
|
+
|
|
204
|
+
test_id: str = Field(..., description="Test identifier")
|
|
205
|
+
task_type: str = Field(..., description="Task type executed")
|
|
206
|
+
tasks_sent: int = Field(..., description="Tasks enqueued")
|
|
207
|
+
tasks_completed: int = Field(0, description="Successfully completed")
|
|
208
|
+
tasks_failed: int = Field(0, description="Failed tasks")
|
|
209
|
+
total_duration_seconds: float = Field(..., description="Total duration")
|
|
210
|
+
overall_throughput_per_second: float = Field(0, description="Overall throughput")
|
|
211
|
+
failure_rate_percent: float = Field(0, description="Failure rate")
|
|
212
|
+
completion_percentage: float = Field(0, description="Completion rate")
|
|
213
|
+
average_throughput_per_second: float = Field(0, description="Average throughput")
|
|
214
|
+
monitor_duration_seconds: float = Field(0, description="Monitor duration")
|
|
215
|
+
batch_size: int = Field(1, description="Batch size used")
|
|
216
|
+
delay_ms: int = Field(0, description="Delay between batches")
|
|
217
|
+
target_queue: str = Field(..., description="Target queue")
|
|
218
|
+
start_time: str | None = Field(None, description="Start time")
|
|
219
|
+
end_time: str | None = Field(None, description="End time")
|
|
220
|
+
task_ids: list[str] = Field(default_factory=list, description="Task IDs")
|
|
221
|
+
|
|
222
|
+
def to_load_test_result(self) -> LoadTestResult:
|
|
223
|
+
"""Transform to standard LoadTestResult format."""
|
|
224
|
+
configuration = LoadTestConfiguration(
|
|
225
|
+
task_type=LoadTestTypes(self.task_type),
|
|
226
|
+
num_tasks=self.tasks_sent,
|
|
227
|
+
batch_size=self.batch_size,
|
|
228
|
+
delay_ms=self.delay_ms,
|
|
229
|
+
target_queue=self.target_queue,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
metrics = LoadTestMetrics(
|
|
233
|
+
tasks_sent=self.tasks_sent,
|
|
234
|
+
tasks_completed=self.tasks_completed,
|
|
235
|
+
tasks_failed=self.tasks_failed,
|
|
236
|
+
total_duration_seconds=self.total_duration_seconds,
|
|
237
|
+
overall_throughput=self.overall_throughput_per_second,
|
|
238
|
+
failure_rate_percent=self.failure_rate_percent,
|
|
239
|
+
completion_percentage=self.completion_percentage,
|
|
240
|
+
average_throughput_per_second=self.average_throughput_per_second,
|
|
241
|
+
monitor_duration_seconds=self.monitor_duration_seconds,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return LoadTestResult(
|
|
245
|
+
status="completed",
|
|
246
|
+
test_id=self.test_id,
|
|
247
|
+
configuration=configuration,
|
|
248
|
+
metrics=metrics,
|
|
249
|
+
start_time=self.start_time,
|
|
250
|
+
end_time=self.end_time,
|
|
251
|
+
task_ids=self.task_ids,
|
|
252
|
+
error=None,
|
|
253
|
+
analysis=None,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class LoadTestErrorModel(BaseModel):
|
|
258
|
+
"""Load test error result with partial information."""
|
|
259
|
+
|
|
260
|
+
task: str = Field(default="load_test_orchestrator", description="Task name")
|
|
261
|
+
status: str = Field(
|
|
262
|
+
...,
|
|
263
|
+
pattern=r"^(failed|timed_out)$",
|
|
264
|
+
description="Error status"
|
|
265
|
+
)
|
|
266
|
+
test_id: str = Field(..., description="Unique test identifier")
|
|
267
|
+
error: str = Field(..., description="Error message")
|
|
268
|
+
partial_info: str | None = Field(None, description="Partial completion info")
|
|
269
|
+
tasks_sent: int | None = Field(None, ge=0, description="Tasks that were sent")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared domain models.
|
|
3
|
+
|
|
4
|
+
Common Pydantic models used across multiple domains and services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseResponse(BaseModel):
|
|
13
|
+
"""Base API response model."""
|
|
14
|
+
|
|
15
|
+
success: bool = Field(..., description="Whether operation was successful")
|
|
16
|
+
message: str = Field(..., description="Response message")
|
|
17
|
+
data: dict[str, Any] | list[Any] | None = Field(None, description="Response data")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorResponse(BaseModel):
|
|
21
|
+
"""Standard error response model."""
|
|
22
|
+
|
|
23
|
+
error: str = Field(..., description="Error message")
|
|
24
|
+
status: str = Field(default="error", description="Status indicator")
|
|
25
|
+
details: dict[str, Any] | None = Field(None, description="Additional error details")
|
|
26
|
+
timestamp: str | None = Field(None, description="ISO timestamp when error occurred")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System domain - health monitoring, alerts, and system management.
|
|
3
|
+
|
|
4
|
+
This domain provides functions for:
|
|
5
|
+
- System health checking and monitoring
|
|
6
|
+
- Component status validation
|
|
7
|
+
- Alert management and notifications
|
|
8
|
+
- System resource monitoring
|
|
9
|
+
|
|
10
|
+
All functions use Pydantic models for type safety and validation.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .alerts import (
|
|
14
|
+
send_alert,
|
|
15
|
+
send_critical_alert,
|
|
16
|
+
send_health_alert,
|
|
17
|
+
)
|
|
18
|
+
from .health import (
|
|
19
|
+
check_system_status,
|
|
20
|
+
get_system_status,
|
|
21
|
+
is_system_healthy,
|
|
22
|
+
register_health_check,
|
|
23
|
+
)
|
|
24
|
+
from .models import (
|
|
25
|
+
Alert,
|
|
26
|
+
AlertSeverity,
|
|
27
|
+
ComponentStatus,
|
|
28
|
+
ComponentStatusType,
|
|
29
|
+
DetailedHealthResponse,
|
|
30
|
+
HealthResponse,
|
|
31
|
+
SystemStatus,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Health functions
|
|
36
|
+
"check_system_status",
|
|
37
|
+
"get_system_status",
|
|
38
|
+
"is_system_healthy",
|
|
39
|
+
"register_health_check",
|
|
40
|
+
# Alert functions
|
|
41
|
+
"send_alert",
|
|
42
|
+
"send_critical_alert",
|
|
43
|
+
"send_health_alert",
|
|
44
|
+
# Models
|
|
45
|
+
"Alert",
|
|
46
|
+
"AlertSeverity",
|
|
47
|
+
"ComponentStatus",
|
|
48
|
+
"ComponentStatusType",
|
|
49
|
+
"SystemStatus",
|
|
50
|
+
"HealthResponse",
|
|
51
|
+
"DetailedHealthResponse",
|
|
52
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System alert management functions.
|
|
3
|
+
|
|
4
|
+
Pure functions for sending alerts, managing notifications, and rate limiting.
|
|
5
|
+
All functions use Pydantic models for type safety and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from app.core.log import logger
|
|
12
|
+
|
|
13
|
+
from .models import Alert, SystemStatus, alert_severity
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Global state for alert rate limiting
|
|
20
|
+
_last_alerts: dict[str, datetime] = {}
|
|
21
|
+
_rate_limit_seconds = 300 # 5 minutes between similar alerts
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _should_send_alert(alert_key: str) -> bool:
|
|
25
|
+
"""Check if we should send this alert based on rate limiting."""
|
|
26
|
+
if alert_key not in _last_alerts:
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
time_since_last = datetime.utcnow() - _last_alerts[alert_key]
|
|
30
|
+
return time_since_last.total_seconds() > _rate_limit_seconds
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def send_alert(alert: Alert) -> None:
|
|
34
|
+
"""Send an alert through configured channels."""
|
|
35
|
+
alert_key = f"{alert.severity}:{alert.title}"
|
|
36
|
+
|
|
37
|
+
if not _should_send_alert(alert_key):
|
|
38
|
+
logger.debug(f"Rate limiting alert: {alert_key}")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
_last_alerts[alert_key] = datetime.utcnow()
|
|
42
|
+
|
|
43
|
+
# Log-based alerting (always available)
|
|
44
|
+
log_level = {
|
|
45
|
+
alert_severity.INFO: logger.info,
|
|
46
|
+
alert_severity.WARNING: logger.warning,
|
|
47
|
+
alert_severity.ERROR: logger.error,
|
|
48
|
+
alert_severity.CRITICAL: logger.critical,
|
|
49
|
+
}.get(alert.severity, logger.info)
|
|
50
|
+
|
|
51
|
+
log_level(
|
|
52
|
+
f"🚨 ALERT [{alert.severity.upper()}]: {alert.title}",
|
|
53
|
+
extra={
|
|
54
|
+
"alert_message": alert.message,
|
|
55
|
+
"alert_metadata": alert.metadata,
|
|
56
|
+
"alert_timestamp": alert.timestamp.isoformat(),
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# TODO: Add integrations for:
|
|
61
|
+
# - Slack/Discord webhooks
|
|
62
|
+
# - Email notifications
|
|
63
|
+
# - PagerDuty/Opsgenie
|
|
64
|
+
# - Custom webhook endpoints
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def send_health_alert(status: SystemStatus) -> None:
|
|
68
|
+
"""Send health-related alerts."""
|
|
69
|
+
if not status.overall_healthy:
|
|
70
|
+
alert = Alert(
|
|
71
|
+
severity=alert_severity.WARNING,
|
|
72
|
+
title="System Health Issues Detected",
|
|
73
|
+
message=(
|
|
74
|
+
f"{len(status.unhealthy_components)} components unhealthy: "
|
|
75
|
+
f"{', '.join(status.unhealthy_components)}"
|
|
76
|
+
),
|
|
77
|
+
timestamp=status.timestamp,
|
|
78
|
+
metadata={
|
|
79
|
+
"unhealthy_components": status.unhealthy_components,
|
|
80
|
+
"health_percentage": status.health_percentage,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
await send_alert(alert)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def send_critical_alert(title: str, message: str) -> None:
|
|
87
|
+
"""Send a critical system alert."""
|
|
88
|
+
alert = Alert(
|
|
89
|
+
severity=alert_severity.CRITICAL,
|
|
90
|
+
title=title,
|
|
91
|
+
message=message,
|
|
92
|
+
timestamp=datetime.utcnow(),
|
|
93
|
+
)
|
|
94
|
+
await send_alert(alert)
|