Skip to content

Commit af8d4e1

Browse files
committed
fix(api): update system router prefix and test paths
- Add /system prefix to system router - Update test paths to match new router prefix - Remove duplicate root endpoint - Update API documentation in README
1 parent 26a52c8 commit af8d4e1

File tree

14 files changed

+409
-90
lines changed

14 files changed

+409
-90
lines changed

README.md

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,165 @@
1-
docker build -t stressed-pod . && docker run -p 5000:5000 stressed-pod
1+
# Stressed Pod
2+
3+
## Overview
4+
Stressed Pod is a tool designed to simulate various types of loads on a Kubernetes pod. It enables testing application resilience by generating CPU, memory loads, and producing custom logs. This tool is particularly useful for performance testing, resource sizing, and scaling policy validation.
5+
6+
## Features
7+
- **CPU Load Management**: Precise control of CPU usage (0 to N cores)
8+
- **Memory Load Management**: Memory consumption simulation (in MB)
9+
- **Dynamic Load**: Progressive variation of CPU/memory load
10+
- **Log Generation**: Custom log production with different levels and formats
11+
- **Kubernetes Probes**: Control of readiness/liveness probes
12+
- **REST API**: HTTP interface for load control
13+
- **Environment Variable Configuration**: Flexible behavior parameterization
14+
15+
## Installation
16+
17+
### Requirements
18+
- Python 3.13+
19+
- Poetry for dependency management
20+
21+
### Configuration
22+
23+
#### Environment Variables
24+
```yaml
25+
# CPU Load
26+
ENABLE_DYNAMIC_CPU_LOAD: "false"
27+
INITIAL_CPU_LOAD: "0"
28+
FINAL_CPU_LOAD: "0.5"
29+
CPU_LOAD_DURATION: "60"
30+
STOP_CPU_LOAD_AT_END: "true"
31+
32+
# Memory Load
33+
ENABLE_DYNAMIC_MEMORY_LOAD: "false"
34+
INITIAL_MEMORY_LOAD: "0"
35+
FINAL_MEMORY_LOAD: "256"
36+
MEMORY_LOAD_DURATION: "60"
37+
STOP_MEMORY_LOAD_AT_END: "true"
38+
39+
# Log Configuration
40+
ENABLE_AUTOMATIC_LOGS: "false"
41+
LOG_MESSAGE: "Automatic log message"
42+
LOG_LEVEL: "info"
43+
LOG_SERVICE: "auto-logger"
44+
LOG_FORMAT: "json"
45+
LOG_INTERVAL: "5"
46+
LOG_DURATION: "60"
47+
48+
# Initial Probe States
49+
READINESS_STATUS: "SUCCESS"
50+
LIVENESS_STATUS: "SUCCESS"
51+
52+
# System Configuration
53+
ENABLE_AUTO_TERMINATION: "false"
54+
AUTO_TERMINATION_DELAY: "300"
55+
```
56+
57+
## API Endpoints
58+
59+
### Load Management
60+
- `GET /load`: Current load status
61+
- `POST /load/cpu/start`: Start CPU load
62+
- `POST /load/cpu/stop`: Stop CPU load
63+
- `POST /load/cpu/dynamic`: Configure dynamic CPU load
64+
- `POST /load/memory/start`: Start memory load
65+
- `POST /load/memory/stop`: Stop memory load
66+
- `POST /load/memory/dynamic`: Configure dynamic memory load
67+
68+
### Log Management
69+
- `POST /log`: Create custom logs
70+
71+
### Probe Management
72+
- `GET /probes`: Probe status
73+
- `GET /probes/readiness`: Readiness probe status
74+
- `GET /probes/liveness`: Liveness probe status
75+
- `POST /probes/status`: Modify probe status
76+
77+
### System Management
78+
- `GET /system`: System information
79+
- `POST /system/terminate`: Schedule pod termination
80+
81+
## Usage Examples
82+
83+
### Dynamic CPU Load
84+
```bash
85+
curl -X POST http://localhost:8000/load/cpu/dynamic \
86+
-H "Content-Type: application/json" \
87+
-d '{
88+
"start_value": 0.1,
89+
"end_value": 0.8,
90+
"duration": 60,
91+
"stop_at_end": true
92+
}'
93+
```
94+
95+
### Memory Load
96+
```bash
97+
curl -X POST http://localhost:8000/load/memory/start \
98+
-H "Content-Type: application/json" \
99+
-d '{
100+
"value": 256
101+
}'
102+
```
103+
104+
### Log Generation
105+
```bash
106+
curl -X POST http://localhost:8000/log \
107+
-H "Content-Type: application/json" \
108+
-d '{
109+
"message": "Test message",
110+
"level": "info",
111+
"service": "test-service",
112+
"format": "json",
113+
"interval": 5,
114+
"duration": 60
115+
}'
116+
```
117+
118+
### Probe Control
119+
```bash
120+
curl -X POST http://localhost:8000/probes/status \
121+
-H "Content-Type: application/json" \
122+
-d '{
123+
"probe": "readiness",
124+
"status": "error"
125+
}'
126+
```
127+
128+
## Development
129+
130+
### Launch app
131+
docker-compose up --build
132+
133+
### Running Tests
134+
```bash
135+
# Install dependencies
136+
poetry install
137+
138+
# Run tests
139+
poetry run pytest
140+
141+
# Run tests with coverage
142+
poetry run pytest --cov=app
143+
```
144+
145+
### Code Quality
146+
The project uses:
147+
- pytest for testing
148+
- black for code formatting
149+
- flake8 for linting
150+
- mypy for type checking
151+
152+
## Contributing
153+
Contributions are welcome! Please feel free to submit a Pull Request.
154+
155+
## License
156+
This project is licensed under the MIT License.
157+
158+
## Important Notes
159+
- The CPU load is distributed across all available cores
160+
- Memory load is specified in MB
161+
- Log formats supported: JSON and plaintext
162+
- Probe status changes are immediate
163+
- All durations are in seconds
164+
165+
This tool is designed for testing purposes and should be used with caution in production environments.

app/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from .routers import load_router, probes_router, system_router, log_router
33

44
app = FastAPI(
5-
title="Load Testing API",
6-
description="API for managing CPU and memory loads, and lifecycle probes",
5+
title="Stressed API",
6+
description="The API simulates controlled workloads and failures to stress-test a system, assessing its resilience, performance, and recovery capabilities.",
77
version="1.0.0",
88
)
99

app/managers/load_manager.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import signal
33
import subprocess
44
from threading import Timer
5+
import psutil
56

67

78
class LoadManager:
@@ -28,7 +29,10 @@ def __init__(self):
2829
self.cpu_duration = int(os.getenv("CPU_LOAD_DURATION", 60))
2930
self.stop_cpu_at_end = os.getenv("STOP_CPU_LOAD_AT_END", "true") == "true"
3031

31-
# Call dynamic load functions during initialization if enabled
32+
self.max_duration = 3600
33+
self.system_memory = psutil.virtual_memory().total // (1024 * 1024)
34+
self.system_cpus = os.cpu_count()
35+
3236
if os.getenv("ENABLE_DYNAMIC_MEMORY_LOAD", "false") == "true":
3337
self.dynamic_memory_load(
3438
self.memory_at_start,
@@ -62,10 +66,12 @@ def stop_cpu_load(self):
6266
self.cpu_timers.clear()
6367
self.cpu_requested = 0
6468

65-
def add_cpu_load(self, value: int):
66-
"""Add CPU load"""
67-
if value < 0:
69+
def add_cpu_load(self, value: float):
70+
"""Add CPU load with validation"""
71+
if value <= 0:
6872
raise ValueError("CPU load must be greater than 0")
73+
if value > self.system_cpus:
74+
raise ValueError(f"CPU load cannot exceed system CPU count ({self.system_cpus})")
6975

7076
if not os.path.exists(self.cpu_script_path):
7177
raise RuntimeError("CPU stress script is missing")
@@ -93,9 +99,11 @@ def stop_memory_load(self):
9399
raise RuntimeError(f"Failed to terminate memory stress: {e}")
94100

95101
def add_memory_load(self, value: int):
96-
"""Add memory load"""
97-
if value < 0:
102+
"""Add memory load with validation"""
103+
if value <= 0:
98104
raise ValueError("Memory load must be greater than 0")
105+
if value > self.system_memory:
106+
raise ValueError(f"Memory load cannot exceed system memory ({self.system_memory}MB)")
99107

100108
if not os.path.exists(self.memory_script_path):
101109
raise RuntimeError("Memory stress script is missing")
@@ -111,9 +119,11 @@ def add_memory_load(self, value: int):
111119
def dynamic_memory_load(
112120
self, start_value: int, end_value: int, duration: int, stop_at_end: bool = False
113121
):
114-
"""Dynamic memory load"""
115-
if duration < 0:
116-
raise ValueError("Duration must be greater than 0")
122+
"""Dynamic memory load with validation"""
123+
if duration <= 0 or duration > self.max_duration:
124+
raise ValueError(f"Duration must be between 1 and {self.max_duration} seconds")
125+
if end_value < start_value:
126+
raise ValueError("End value must be greater than start value")
117127

118128
try:
119129
start_value = start_value
@@ -149,11 +159,13 @@ def apply_dynamic_memory_load(interval_num):
149159
apply_dynamic_memory_load(0)
150160

151161
def dynamic_cpu_load(
152-
self, start_value: int, end_value: int, duration: int, stop_at_end: bool = False
162+
self, start_value: float, end_value: float, duration: int, stop_at_end: bool = False
153163
):
154-
"""Dynamic CPU load"""
155-
if duration < 0:
156-
raise ValueError("Duration must be greater than 0")
164+
"""Dynamic CPU load with validation"""
165+
if duration <= 0 or duration > self.max_duration:
166+
raise ValueError(f"Duration must be between 1 and {self.max_duration} seconds")
167+
if end_value < start_value:
168+
raise ValueError("End value must be greater than start value")
157169

158170
try:
159171
start_value = start_value
@@ -183,7 +195,6 @@ def apply_dynamic_cpu_load(interval_num):
183195
timer.start()
184196

185197
elif stop_at_end:
186-
# Correctly schedule stop_memory_load to execute after the last interval
187198
stop_timer = Timer(10, self.stop_cpu_load)
188199
stop_timer.start()
189200

app/managers/log_manager.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import logging
22
import json
33
from datetime import datetime, UTC
4+
import time
5+
import os
46
from ..models.schemas import LogRequest, LogFormat
57
import asyncio
6-
import time
78

89

910
class JsonFormatter(logging.Formatter):
10-
"""Formatter personnalisé pour les logs JSON"""
11+
"""Custom formatter for JSON logs"""
1112

1213
def format(self, record):
1314
return json.dumps(
@@ -21,7 +22,7 @@ def format(self, record):
2122

2223

2324
class PlainTextFormatter(logging.Formatter):
24-
"""Formatter personnalisé pour le texte plat"""
25+
"""Custom formatter for plain text logs"""
2526

2627
def format(self, record):
2728
return f"{self.formatTime(record)} | {record.levelname} | {getattr(record, 'service', '-')} | {record.getMessage()}"
@@ -36,11 +37,14 @@ def __init__(self):
3637
self.json_formatter = JsonFormatter()
3738

3839
self._setup_handler(LogFormat.JSON)
39-
4040
self.current_format = LogFormat.JSON
4141

42+
# Start automatic logging if enabled
43+
if os.getenv("ENABLE_AUTOMATIC_LOGS", "false").lower() == "true":
44+
self._start_automatic_logs()
45+
4246
def _setup_handler(self, format_type: LogFormat):
43-
"""Configure un nouveau handler avec le format spécifié"""
47+
"""Configure a new handler with the specified format"""
4448
for handler in self.logger.handlers[:]:
4549
self.logger.removeHandler(handler)
4650

@@ -73,6 +77,29 @@ def _validate_interval_duration(self, interval: int | None, duration: int | None
7377
if interval and duration and interval > duration:
7478
raise ValueError("Interval cannot be greater than duration")
7579

80+
def _start_automatic_logs(self):
81+
"""Initialize automatic logging based on environment variables"""
82+
log_data = LogRequest(
83+
message=os.getenv("LOG_MESSAGE", "Automatic log message"),
84+
level=os.getenv("LOG_LEVEL", "info").lower(),
85+
service=os.getenv("LOG_SERVICE", "auto-logger"),
86+
format=LogFormat(os.getenv("LOG_FORMAT", "json").lower()),
87+
interval=int(os.getenv("LOG_INTERVAL", "5")),
88+
duration=int(os.getenv("LOG_DURATION", "60"))
89+
)
90+
91+
# Create single log immediately
92+
asyncio.create_task(self._create_single_log(
93+
log_data,
94+
getattr(logging, log_data.level.upper())
95+
))
96+
97+
# Setup recurring logs if needed
98+
if log_data.interval and log_data.duration:
99+
asyncio.create_task(
100+
self._create_recurring_logs(log_data, log_data.interval, log_data.duration)
101+
)
102+
76103
async def create_log(self, log_data: LogRequest) -> dict | str:
77104
"""Create log"""
78105
level = getattr(logging, log_data.level.upper())
@@ -96,7 +123,7 @@ async def create_log(self, log_data: LogRequest) -> dict | str:
96123
async def _create_recurring_logs(
97124
self, log_data: LogRequest, interval: int, duration: int
98125
):
99-
"""Crée des logs de manière récurrente"""
126+
"""Create logs at regular intervals"""
100127
end_time = time.time() + duration
101128
while time.time() < end_time:
102129
await self._create_single_log(
@@ -105,7 +132,7 @@ async def _create_recurring_logs(
105132
await asyncio.sleep(interval)
106133

107134
async def _create_single_log(self, log_data: LogRequest, level: int) -> dict | str:
108-
"""Crée un seul log"""
135+
"""Create a single log"""
109136
try:
110137
timestamp = datetime.now(UTC).isoformat()
111138

@@ -138,8 +165,8 @@ def _validate_log_level(self, level: str) -> bool:
138165
return level.lower() in valid_levels
139166

140167
async def log_creator(self, log_data: LogRequest, interval: int, duration: int):
141-
"""Ffunction to create multiple log log"""
168+
"""Function to create multiple logs"""
142169
end_time = time.time() + duration
143170
while time.time() < end_time:
144171
await self.create_log(log_data)
145-
await asyncio.sleep(interval)
172+
await asyncio.sleep(interval)

app/managers/system_manager.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import psutil
22
from datetime import datetime, timedelta
3+
import os
4+
import asyncio
35

46

57
class SystemManager:
8+
def __init__(self):
9+
if os.getenv("ENABLE_AUTO_TERMINATION", "false").lower() == "true":
10+
delay = int(os.getenv("AUTO_TERMINATION_DELAY", "300"))
11+
self.terminate(delay)
12+
613
def get_system_info(self):
714
"""Get system information"""
815
return {

0 commit comments

Comments
 (0)