-
Notifications
You must be signed in to change notification settings - Fork 18
feat: add load testing infrastructure with Locust #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # TENET AI Load Testing | ||
|
|
||
| ## Overview | ||
|
|
||
| This directory contains load testing scripts for TENET AI APIs using Locust. | ||
|
|
||
| ## Tested Endpoints | ||
|
|
||
| - GET /health | ||
| - GET /v1/stats | ||
| - GET /v1/events | ||
| - POST /v1/events/llm | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| pip install locust | ||
| ``` | ||
|
|
||
| ## Running Tests | ||
|
|
||
| ```bash | ||
| locust -f tests/load/locustfile.py | ||
| ``` | ||
|
|
||
| Then open: | ||
|
|
||
| http://localhost:8089 | ||
|
|
||
| ## Test Scenarios | ||
|
|
||
| ### Sustained Load Test | ||
|
|
||
| - Target: 100 requests/second | ||
| - Duration: 10 minutes | ||
|
|
||
| ### Spike Load Test | ||
|
|
||
| - Simulate sudden bursts of traffic | ||
| - Increase users rapidly | ||
|
|
||
| ### Concurrent Connection Test | ||
|
|
||
| - Run multiple concurrent users | ||
| - Observe latency and error rates | ||
|
|
||
| ## Metrics to Measure | ||
|
|
||
| - P50 latency | ||
| - P95 latency | ||
| - P99 latency | ||
| - Throughput | ||
| - Error rate | ||
|
|
||
| ## Acceptance Criteria | ||
|
|
||
| - P95 latency < 500 ms under sustained load | ||
| - Error rate < 0.1% under normal load | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||
| from locust import HttpUser, task, between | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| API_KEY = "test-api-key" | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace hardcoded API key with environment variable. The hardcoded 🔐 Proposed fix using environment variable+import os
from locust import HttpUser, task, between
-API_KEY = "test-api-key"
+API_KEY = os.getenv("TENET_LOAD_TEST_API_KEY", "test-api-key")Then document in README.md: export TENET_LOAD_TEST_API_KEY="your-valid-test-key"
locust -f tests/load/locustfile.py --host=http://localhost:8000📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Hardcoding Prompt for AI agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class TENETLoadUser(HttpUser): | ||||||||||||||||||||||
| wait_time = between(1, 3) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| headers = { | ||||||||||||||||||||||
| "x-api-key": API_KEY, | ||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+9
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convert mutable class attribute to property or instance attribute. Ruff correctly flags that defining 🛡️ Recommended fix using on_start- headers = {
- "x-api-key": API_KEY,
- "Content-Type": "application/json",
- }
+
+ def on_start(self):
+ """Initialize headers per user instance."""
+ self.headers = {
+ "x-api-key": API_KEY,
+ "Content-Type": "application/json",
+ }📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.15)[warning] 9-12: Mutable default value for class attribute (RUF012) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @task(3) | ||||||||||||||||||||||
| def health_check(self): | ||||||||||||||||||||||
| self.client.get( | ||||||||||||||||||||||
| "/health", | ||||||||||||||||||||||
| headers=self.headers, | ||||||||||||||||||||||
| name="GET /health" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @task(2) | ||||||||||||||||||||||
| def get_stats(self): | ||||||||||||||||||||||
| self.client.get( | ||||||||||||||||||||||
| "/v1/stats", | ||||||||||||||||||||||
| headers=self.headers, | ||||||||||||||||||||||
| name="GET /v1/stats" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @task(2) | ||||||||||||||||||||||
| def list_events(self): | ||||||||||||||||||||||
| self.client.get( | ||||||||||||||||||||||
| "/v1/events", | ||||||||||||||||||||||
| headers=self.headers, | ||||||||||||||||||||||
| name="GET /v1/events" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
+22
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add response validation and error handling to authenticated endpoints. The ✅ Proposed enhancement with response validation `@task`(2)
def get_stats(self):
- self.client.get(
+ with self.client.get(
"/v1/stats",
headers=self.headers,
- name="GET /v1/stats"
- )
+ name="GET /v1/stats",
+ catch_response=True
+ ) as response:
+ if response.status_code != 200:
+ response.failure(f"Got status code {response.status_code}")
+ elif "total_events" not in response.json():
+ response.failure("Missing expected field: total_events")
+ else:
+ response.success()
`@task`(2)
def list_events(self):
- self.client.get(
+ with self.client.get(
"/v1/events",
headers=self.headers,
- name="GET /v1/events"
- )
+ name="GET /v1/events",
+ catch_response=True
+ ) as response:
+ if response.status_code != 200:
+ response.failure(f"Got status code {response.status_code}")
+ elif "events" not in response.json():
+ response.failure("Missing expected field: events")
+ else:
+ response.success()🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @task(1) | ||||||||||||||||||||||
| def submit_llm_event(self): | ||||||||||||||||||||||
| payload = { | ||||||||||||||||||||||
| "source_type": "chat", | ||||||||||||||||||||||
| "source_id": "load-test-user", | ||||||||||||||||||||||
| "model": "gpt-4", | ||||||||||||||||||||||
| "prompt": "This is a load testing request", | ||||||||||||||||||||||
| "system_prompt": "You are a helpful assistant", | ||||||||||||||||||||||
| "metadata": { | ||||||||||||||||||||||
| "environment": "load-test" | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| self.client.post( | ||||||||||||||||||||||
| "/v1/events/llm", | ||||||||||||||||||||||
| json=payload, | ||||||||||||||||||||||
| headers=self.headers, | ||||||||||||||||||||||
| name="POST /v1/events/llm" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document required host configuration and API key setup.
The running instructions omit critical setup details: users must specify the target host URL via
--hostand ensure a valid API key is configured. Without these, the load tests will fail.📝 Suggested documentation addition
Then open:
http://localhost:8089
+
+### Authentication
+
+Update the
API_KEYconstant inlocustfile.pywith a valid test API key that has:+-
ingestpermission for POST /v1/events/llm+-
readpermission for GET /v1/stats and GET /v1/eventsVerify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
@tests/load/README.mdaround lines 20 - 28, Update the tests/load/README.mdto include required host and auth setup: instruct users to pass --host (example:
--host=http://localhost:8000) when running locust with tests/load/locustfile.py
and add an "Authentication" section telling them to set the API_KEY constant in
locustfile.py to a valid test API key with the needed permissions (ingest for
POST /v1/events/llm and read for GET /v1/stats and GET /v1/events); reference
locustfile.py and the API_KEY constant so it's clear where to configure these
values.