Why is the framework structured this way?
Designing a maintainable performance testing framework is about much more than generating HTTP traffic. This document explains the architectural decisions behind the framework and why each component exists.
Most examples of k6 projects consist of several scenario files and a single entry point. While this approach works well for stateless HTTP testing, it quickly becomes difficult to maintain when the system under test introduces state, asynchronous communication, external infrastructure and reporting requirements.
This project intentionally separates responsibilities between multiple k6 scripts and components.
Instead of treating performance testing as "running scripts", it treats it as a distributed system composed of cooperating services.
The architecture was designed around four principles:
- realistic workload simulation
- loose coupling between components (separation of concerns)
- observability-first design
- maintainable reporting pipeline
Rather than optimizing for the shortest possible implementation, the framework is optimized for long-term extensibility.
Note: low-level quick link
The framework consists of five logical layers.
Load Generation
↓
Coordination
↓
Metrics Collection
↓
Reporting
↓
Visualization
Each layer has a clearly defined responsibility and communicates with the next through explicit interfaces.
graph TB
subgraph k6["k6 Load Test"]
ClientVU["Client VUs"]
AgentVU["Agent VUs"]
WSListener["WS Listener VUs<br/>(1 per agent)"]
Monitors["Monitors<br/>(DB, Activity, WS Events)"]
end
subgraph RedisCoord["Redis Coordination Layer<br/>(Shared State + Queues + Streams)"]
SharedValues["Shared Values<br/>(VU counts, counters<br/>worker states, boundaries)"]
Queues["Queues<br/>(token handoff<br/>event pairing)"]
Streams["Streams<br/>(expected vs actual<br/>event records)"]
end
subgraph External["External Systems"]
App["Application<br/>(HTTP)"]
PG["Postgres<br/>(state)"]
K8s["Kubernetes<br/>(optional)"]
Prom["Prometheus<br/>(optional)"]
end
subgraph InfluxDB["InfluxDB<br/>(time-series metrics)"]
HTTPMetrics["HTTP Metrics<br/>(duration, errors)"]
DBMetrics["DB Metrics<br/>(connections, queries)"]
WSMetrics["WS Metrics<br/>(latency, success)"]
GaugeMetrics["Gauges<br/>(activity, connections)"]
end
subgraph Reporting["Reporting Service<br/>(Node.js Express)"]
Orchestrator["Orchestrator<br/>(routes, validates)"]
InfluxQuery["InfluxDB Query<br/>Collector"]
ResourceQuery["K8s/Prom Query<br/>Collector"]
CSVBuilder["CSV Builder<br/>(per-group, per-period)"]
end
subgraph Output["Output"]
CSVFiles["CSV Files<br/>(results/)"]
GeneralInfo["GeneralInfo.csv<br/>(pod specs, config)"]
Resources["Resources/<br/>(CPU, Memory)"]
end
ClientVU -->|HTTP| App
AgentVU -->|HTTP| App
AgentVU -->|Query| PG
WSListener -->|WebSocket| App
ClientVU -->|Read/Write| SharedValues
AgentVU -->|Read/Write| SharedValues
WSListener -->|Read/Write| Queues
WSListener -->|Read/Write| Streams
AgentVU -->|Write| Queues
Monitors -->|Read| SharedValues
Monitors -->|Read| Streams
Monitors -->|Query| PG
ClientVU -->|Push| InfluxDB
AgentVU -->|Push| InfluxDB
WSListener -->|Push| InfluxDB
Monitors -->|Push| InfluxDB
InfluxDB -->|Query| InfluxQuery
K8s -->|API| ResourceQuery
Prom -->|API| ResourceQuery
SharedValues -->|Read| InfluxQuery
InfluxQuery -->|Series| CSVBuilder
ResourceQuery -->|Metrics| CSVBuilder
CSVBuilder -->|Write| CSVFiles
CSVBuilder -->|Write| GeneralInfo
CSVBuilder -->|Write| Resources
style k6 fill:#4A90E2,stroke:#333,color:#fff
style RedisCoord fill:#DC143C,stroke:#fff,stroke-width:3px,color:#fff
style External fill:#F5A623,stroke:#333,color:#fff
style InfluxDB fill:#7ED321,stroke:#333,color:#fff
style Reporting fill:#BD10E0,stroke:#333,color:#fff
style Output fill:#50E3C2,stroke:#333,color:#fff
style SharedValues fill:#FF6B6B,stroke:#fff,stroke-width:2px,color:#fff
style Queues fill:#FF6B6B,stroke:#fff,stroke-width:2px,color:#fff
style Streams fill:#FF6B6B,stroke:#fff,stroke-width:2px,color:#fff
Before discussing individual components, it is worth explaining the principles that guided the architecture.
Every major component and k6 script performs one job.
- k6 generates workload.
- Redis synchronizes distributed virtual users.
- InfluxDB stores time-series metrics.
- The reporting service transforms raw metrics into reports.
- Chronograf visualizes collected metrics.
Keeping responsibilities isolated makes individual parts easier to replace without affecting the entire system.
The primary objective of the framework is not to produce response times.
Instead, it aims to explain system behaviour under realistic load.
Whenever performance decreases, I want to answer questions such as:
- Which operation became slower?
- Was the slowdown visible on every transaction?
- Was infrastructure under pressure?
- Did database activity increase?
- Were WebSocket notifications delayed?
- Were virtual users behaving correctly?
The same goes even if performance improves, I want to answer questions such as:
- Which operation became faster?
- Was it a new database index?
- Was it because some business logic block is broken?
- Was it because assignment distribution algorithms stopped working?
- Do user actions generate events?
Those questions influenced almost every architectural decision.
Distributed systems become difficult to debug when components communicate implicitly.
Whenever possible, this framework uses explicit communication channels.
Examples include:
- Redis queues
- Redis streams
- HTTP APIs
- InfluxDB measurements
Having visible communication paths makes troubleshooting significantly easier during long-running performance tests.
The framework supports both single-worker and multi-worker execution without code changes.
Key principles for distributed mode:
- Minimal per-worker overhead – Blocked monitors consume near-zero resources
- Single metric stream – Only one worker sends monitoring metrics to InfluxDB
- Explicit worker coordination – All state visible in Redis (election, completion, boundaries)
- Correct test windows – Test boundaries span earliest setup to latest teardown across all workers
- Idempotent reporting – Reporting service waits for all workers before triggering once
This allows seamless scaling from 1 to 10+ workers without architectural changes.
The framework supports seamless scaling from single-worker to multi-worker execution through an elegant single-sender monitor architecture.
In distributed mode, multiple k6 instances run simultaneously on different machines/containers, all coordinating through shared Redis and InfluxDB.
Problem Without Single-Sender Pattern:
- 3 workers × 4 monitors = 12 metric streams for what should be 4 global metrics
- InfluxDB receives duplicate data (3 copies of each metric)
- Query-level aggregation required:
SELECT SUM(metric) GROUP BY worker - High cardinality, confusion, inefficient storage
Solution: Single-Sender Architecture
Only ONE worker's monitors run and emit metrics to InfluxDB. That worker aggregates data from ALL workers via Redis.
At setup completion (each worker runs setup independently):
- All workers initialize their monitor state as "blocked" in Redis:
monitor_workers_state:{BUILD_TAG} - Each worker registers itself as "working" in:
reporting_workers_state:{BUILD_TAG} - At setup end, if ALL workers are blocked, one worker is randomly elected to become "unblocked"
- Election uses:
Math.random() < 1 / workerCount→ exactly one winner
Example with 3 workers:
Time: Setup phase
Worker 1 completes setup:
→ register in monitor_workers_state as "blocked"
→ check: all 3 blocked? YES (1/3 done)
→ random: unblock? YES (lucky!)
→ becomes PRIMARY MONITOR (unblocked)
Worker 2 completes setup:
→ register in monitor_workers_state as "blocked"
→ check: all 3 blocked? NO (Worker 1 unblocked)
→ stays BLOCKED (skips monitoring)
Worker 3 completes setup:
→ register in monitor_workers_state as "blocked"
→ check: all 3 blocked? NO (Worker 1 unblocked)
→ stays BLOCKED (skips monitoring)
Result: monitor_workers_state = {
worker_abc: "unblocked", ← Primary monitor
worker_def: "blocked",
worker_ghi: "blocked"
}
The unblocked worker aggregates metrics across all workers and emits single streams to InfluxDB.
Per-monitor aggregation logic:
| Monitor | Aggregation Method | Result |
|---|---|---|
| agent-activity-monitor.js | Sums agent_vu_number:{w} and client_vu_number:{w} from ALL workers; reads global agent_activities hash |
Single global VU count + global activity % |
| agent-ws-monitor.js | Reads global counters: agents_ws_hit, agents_ws_success, agents_ws_error (already aggregated) |
Single WS event stream |
| conversations-monitor.js | Queries Postgres for global conversation counts (system-wide) | Single conversation throughput stream |
| db-connections-monitor-gauge.js | Queries Postgres for global connection pool count | Single connection gauge |
Blocked workers (non-primary) check their blocking state at startup and return immediately.
- Zero overhead – No DB queries, no Redis polling, no monitoring loops
- Clean exit – Early return in each monitor function
- Scales linearly – Adding 10 workers doesn't increase monitoring load
To ensure correct time boundaries across all workers:
-
test_start_ms – Kept as earliest (redisSetIfEarlier)
- Set by first worker's setup completion
- Represents when load generation began
-
test_end_ms – Kept as maximum (redisSetIfLater)
- Set by last worker's teardown completion
- Represents when load generation ended
Example timeline (3 workers):
T=1000ms: Worker 1 setup → test_start_ms = 1000
T=1005ms: Worker 2 setup → test_start_ms unchanged (1000 earlier)
T=1010ms: Worker 3 setup → test_start_ms unchanged (1000 earlier)
Load phase (all workers run scenarios)
T=2300ms: Worker 1 teardown → test_end_ms = 2300
T=2305ms: Worker 2 teardown → test_end_ms = 2305 (replaces, newer)
T=2310ms: Worker 3 teardown → test_end_ms = 2310 (replaces, newer)
Result: Test window = [1000ms, 2310ms]
Spans all worker activity correctly ✓
When workers complete teardown:
- Each worker marks itself as "done" in Redis:
reporting_workers_state:{BUILD_TAG} - Each worker calls POST
/start-reporting(non-blocking) - Reporting Service waits for ALL workers to mark "done" OR times out after 60 seconds
- When all workers done, service triggers reporting ONCE
- Reporting queries InfluxDB using correct test window boundaries
Prevents:
- ❌ Multiple CSV generations (reports only once)
- ❌ Incorrect time windows (uses min/max from all workers)
- ❌ Partial data (waits for all workers)
✅ No InfluxDB duplication – Each metric type has one stream, not N
✅ Clean queries – No WHERE worker='xyz' or GROUP BY clauses needed
✅ True global metrics – Activity %, connection count represent entire system
✅ Minimal overhead – Blocked workers exit immediately
✅ Automatic failover – If primary monitor crashes, remaining workers detect unchanged state
✅ Scales gracefully – Works with 2 workers or 100+ workers
Single-sender architecture enables multiple deployment patterns:
| Pattern | Setup | Notes |
|---|---|---|
| Single-worker | No election, all monitors run | Simplest, backward compatible |
| Sequential start | Workers join over minutes | Good for observing ramp-up |
| Simultaneous start | All workers start together | For synchronized load spike |
| Staggered regions | Workers in different geographic locations | Multi-regional testing |
| Kubernetes | k6-operator creates pods dynamically | Cloud-native scaling |
The load generation layer is responsible for simulating realistic user behaviour.
Unlike simple HTTP benchmark scripts, scenarios maintain business state throughout execution.
Virtual users perform actions such as:
- authentication
- conversation creation
- polling assigned tasks
- message exchange
- WebSocket interaction
- think time simulation
This produces workloads that more closely resemble real production traffic.
One limitation of k6 is that virtual users cannot directly exchange information.
That becomes problematic whenever one virtual user depends on work performed by another.
Examples include:
- passing authentication tokens
- matching agents with conversations
- sharing WebSocket information
- tracking asynchronous events
Redis solves this problem by acting as a lightweight synchronization layer.
Instead of introducing hidden global state, every interaction passes through an explicit coordination mechanism.
The reasoning behind choosing Redis is discussed in:
Every significant event inside the framework produces metrics.
These include traditional performance metrics such as:
- response time
- throughput
- error rate
but also operational metrics including:
- active agents
- active clients
- WebSocket latency
- database connections
- conversation lifecycle events
Separating business metrics from infrastructure and traditional metrics allows performance investigations to move beyond simple latency analysis.
The reasoning behind metrics logic is discussed in:
Raw time-series data is excellent for visualization but rarely suitable for comparison between test executions.
The reporting service transforms raw metrics into structured datasets.
Its responsibilities include:
- querying InfluxDB
- collecting infrastructure metrics
- populating InfluxDB with infrastructure data
- separating execution phases
- generating CSV reports
- organizing output by category
Moving this logic outside k6 keeps load generation focused solely on simulation.
Moreover, it was the most logical solution for in-built k6 reporting limitations which can happen on scale:
Chronograf dashboards provide immediate feedback during test execution.
Rather than displaying a single response time graph, dashboards combine:
- infrastructure utilisation
- business metrics
- database behaviour
- transaction latency
- WebSocket metrics
This makes dashboards useful not only during testing, but also during investigation sessions.
Time-series graphs are excellent for identifying trends, but they often hide the bigger picture.
Even relatively short test executions can produce thousands of points that are difficult to compare visually.
Aggregation dashboards complement those graphs by highlighting statistical characteristics rather than individual samples.
The architecture intentionally leaves room for future improvements.
Be it new feature or extension of existing ones.
Addition of new metric?
as simple as adding query to the json
Addition of new script, even if it is a whole new scenario?
write it and wire to the orchestrator
Possible extensions include:
- distributed execution
- Grafana dashboards
- OpenTelemetry export
- automatic regression comparison
- HTML report generation
- cloud-native deployment
Because components communicate through well-defined interfaces, adding new functionality usually requires extending only one layer rather than modifying the entire framework.
The biggest lesson during development was that performance testing frameworks eventually become software projects of their own.
Once monitoring, reporting, synchronization and business workflows are introduced, treating the framework as a collection of scripts becomes increasingly difficult.
Therefore, using good old programming principles like SOLID is recommended (read MANDATORY).
Designing explicit boundaries between components required more work initially, but resulted in a system that is easier to maintain, easier to debug and significantly easier to extend.
Looking back at the version 1 on JMeter, I would make the same architectural decision again.
k6's execution model: VUs are isolated JavaScript runtimes. Each VU runs its assigned scenario independently. No shared memory between VUs—all coordination happens through external systems (Redis, Postgres, HTTP).
Four Concurrent Scenarios:
-
Clients Scenario (ramping-vus, e.g., 0→50 over 1 min, hold 50 for 4 min)
- VU count: Configurable via ENV CONCURRENT_CLIENTS (default 5)
- Ramp-up: CONCURRENT_CLIENTS / (CLIENT_RAMPUP seconds) VUs per second
- Each VU executes:
a) Create conversation via Node-RED webhook
b) Wait for Postgres (agent assignment) with polling (5s interval, max 30s)
c) Send lock-step message sequence (CONV_MSG_CYCLES iterations)
- Each cycle: bot_entry_logic_msg → bot_language_msg → bot_entry_system_msg → client_conversation_msg → client_conversation_CSAT_msg
- Think time between cycles: 8s (hardcoded) d) Exit
- Metrics recorded: http_req_duration, http_req_failed (by name tag)
- Redis side-effects: client_vu_number (gauge, read by monitors)
-
Agents Scenario (ramping-vus, e.g., 0→10 over 1 min, hold 10 for 4 min)
- VU count: Configurable via ENV CONCURRENT_AGENTS (default 6)
- Ramp-up: CONCURRENT_AGENTS / (AGENT_RAMPUP seconds) VUs per second
- Each VU executes:
a) Login: GET /login_page → POST /signin → GET /main → store bearerToken
b) Push token to Redis: RPUSH ws:queue:pairing:ws {bearerToken, pubsubToken, jwtId, agentId}
c) Lock-step message response sequence (CONV_MSG_CYCLES iterations):
- GET /get_my_conversations
- GET /get_assigned_conversation
- GET /get_conversation_messages
- POST /agent_conversation_msg (reply)
- PUT /agent_entry_update_last_seen
- PUT /agent_conversation_update_last_seen
- PUT /agent_conversation_resolve
- Same think time 8s between cycles d) Exit
- Metrics recorded: http_req_duration, db_query_duration (by name tag)
- Redis side-effects: agent_vu_number, agent_activities hash, ws:stream:event:
-
Agents-WS Scenario (ramping-vus, same ramp as agents)
- VU count: One listener per agent (couples with agents scenario)
- Each VU executes:
a) Block on Redis: BLPOP ws:queue:pairing:ws (timeout 30s) → extract {bearerToken, pubsubToken, jwtId, agentId}
b) Open WebSocket: connect with jwtId + pubsubToken
c) For each message cycle:
- Wait for notification event on WebSocket
- Extract event, timestamp it
- Compare with expected event (from Redis stream ws:stream:event:)
- Record latency: actual_timestamp - expected_timestamp
- Record success/error (matched within TTL or not) d) Push agentId to next coordinator: RPUSH ws:queue:pairing:stream
- Metrics recorded: agents_ws_event_latency (Trend), agents_ws_success, agents_ws_error (Counter/Rate)
-
DB Connection Monitor (constant-vus, 1 VU, runs full duration)
- Executes every 5s: a) SELECT count() FROM pg_stat_activity → db_connections gauge b) SELECT count() FROM conversations WHERE status='unprocessed' → conversations_unprocessed gauge c) SELECT count() FROM conversations WHERE status='resolved' → conversations_processed gauge d) SELECT count() FROM conversations WHERE created_at > now() - interval '1 min' → conversations_created gauge
- No HTTP metrics, only Postgres queries
-
Agent Activity Monitor (constant-vus, 1 VU, runs full duration)
- Executes every 10s: a) Read agent_activities hash from Redis (written by agents scenario) b) Calculate % of agents currently active: (active_count / CONCURRENT_AGENTS) * 100 c) Record as agent_activity gauge
- Aggregates per-VU state into single metric
-
Agents-WS Event Stream Checker (constant-vus, 1 VU, runs full duration)
- Executes every 1s: a) Read all agent IDs from Redis (written by agents-ws scenario) b) For each agent, check ws:queue:event: (actual WS events) c) Compare with ws:stream:event: (expected events) d) Record agents_ws_hit (counted), agents_ws_success, agents_ws_error gauges
- End-to-end event delivery measurement
Redis serves dual purposes:
- Target under test (agents read notifications, customers see updates)
- Cross-VU coordination mechanism
Redis Keys Used (organized by purpose):
| Key | Type | Written By | Read By | Purpose |
|---|---|---|---|---|
agent_vu_number:{workerId} |
String (int) | agents-scenario | agent-activity-monitor | Current active agent VUs on this worker |
client_vu_number:{workerId} |
String (int) | clients-scenario | agent-activity-monitor | Current active client VUs on this worker |
| Key | Type | Written By | Read By | Purpose |
|---|---|---|---|---|
agent_activities |
Hash | agents-scenario | agent-activity-monitor, agents-ws-event-stream-checker | Global agent state: {agentId: 0/1} (0=idle, 1=active) |
agents_ws_hit |
String (int) | agents-ws-event-stream-checker | agent-ws-monitor | Total WS messages received (global counter) |
agents_ws_success |
String (int) | agents-ws-event-stream-checker | agent-ws-monitor | Successfully matched WS events (global counter) |
agents_ws_error |
String (int) | agents-ws-event-stream-checker | agent-ws-monitor | Failed/stale WS events (global counter) |
| Key | Type | Written By | Read By | Purpose |
|---|---|---|---|---|
ws:queue:pairing:ws |
List | agents-scenario | agents-ws-scenario | Agent token handoff (JSON: {bearerToken, pubsubToken, jwtId, agentId}) |
ws:queue:pairing:stream |
List | agents-ws-scenario | agents-ws-event-stream-checker | Agent ID handoff |
ws:stream:event:{agentId} |
Stream | agents-scenario | agents-ws-event-stream-checker | Expected events (timestamp, eventId) |
ws:queue:event:{agentId} |
List | agents-ws-scenario | agents-ws-event-stream-checker | Actual WS events received |
| Key | Type | Written By | Read By | Purpose | Semantics |
|---|---|---|---|---|---|
test_start_ms |
String (int) | setup() (via redisSetIfEarlier) | teardown(), reportingClient.js | Test start time (epoch ms) | Kept as minimum – when first worker started load |
test_end_ms |
String (int) | teardown() (via redisSetIfLater) | reportingClient.js | Test end time (epoch ms) | Kept as maximum – when last worker stopped load |
test_maxload_ms |
String (int) | setup() / agents-scenario | reportingClient.js | Transition to max load time | When all VUs ramped up |
| Key | Type | Written By | Read By | Purpose |
|---|---|---|---|---|
reporting_workers_state:{BUILD_TAG} |
Hash | setup(), teardown() | all workers, reporting service | Worker lifecycle: {workerId: working|done} |
monitor_workers_state:{BUILD_TAG} |
Hash | setup() (election logic) | all monitors | Monitor blocking state: {workerId: blocked|unblocked} |
Example state snapshot during 3-worker test:
reporting_workers_state:build-1 = {
worker_abc123: "working",
worker_def456: "working",
worker_ghi789: "working"
}
monitor_workers_state:build-1 = {
worker_abc123: "unblocked", ← Primary monitor (elected)
worker_def456: "blocked", ← Skips monitoring
worker_ghi789: "blocked" ← Skips monitoring
}
All values are JSON-encoded/decoded transparently by redisClient.js.
All scenarios push metrics to InfluxDB via k6's --out influxdb=... flag.
Metric Types:
-
Trend: http_req_duration, db_query_duration, agents_ws_event_latency
- InfluxDB stores: mean, min, max, p95, p99
-
Rate: http_req_failed, agents_ws_error
- InfluxDB stores: count, rate (per second)
-
Counter: http_reqs
- InfluxDB stores: accumulated count
-
Gauge: agent_vu_number, client_vu_number, agent_activity, db_connections, conversations_, agents_ws_
- InfluxDB stores: last value per time bucket
InfluxDB Database: k6 (default) Retention Policy: k6_test_rp (1 day by default)
Post-test orchestrator that queries InfluxDB, correlates metrics, and generates CSV reports.
Entry Point: POST /start-reporting
Request Payload Structure:
{
"testWindow": {
"startMs": 1679788140000,
"maxLoadMs": 1679788260000,
"endMs": 1679788380000
},
"BUILD_TAG": "test1",
"queryList": {...}, // from reporting/query_list.json
"DATA_INTERVAL": "5",
"SPREAD_INTERVAL": "15",
"INFLUXDB_URL": "http://localhost:8086",
"INFLUXDB_DB": "k6",
"OUTPUT_DIR": "../results",
"CONCURRENT_AGENTS": "10",
"AGENT_RAMPUP": "40s",
"CONCURRENT_CLIENTS": "50",
"CLIENT_RAMPUP": "30s",
"REDIS_ADDRESS": "localhost:6379",
"CONV_MSG_CYCLES": "6",
"KUBE_DOMAIN_NAME": "k8s.internal", // optional
"KUBE_NAMESPACE": "default", // optional
"KUBE_API_AUTH_TOKEN": "Bearer ...", // optional
"PROM_DOMAIN_NAME": "prometheus.internal", // optional
"PROM_STEP": "15" // optional (seconds)
}Processing Flow:
-
Validation (validation.js)
- Required fields: testWindow, BUILD_TAG, queryList, DATA_INTERVAL, SPREAD_INTERVAL, INFLUXDB_URL, OUTPUT_DIR, CONV_MSG_CYCLES
- Resource fields (KUBE_,PROM_): all-or-nothing (all present or all absent)
- Intervals: must be valid integers
-
Orchestration (reportingOrchestrator.js)
- Loop each category in queryList.types
- Determine period strategy: CATEGORY_PERIOD_MAP
- Call collectMetricsByPeriod() with category-specific rules
-
InfluxDB Collection (influxReporter.js)
- For each graph in category:
- For each period (or flat if null):
- For each group in graph:
- Determine interval: group.name.includes('spread') ? 15s : 5s
- Build query with placeholders: dateTimeStart, dateTimeEnd, interval, CONV_MSG_CYCLES
- Execute substitutePlaceholders(query, bounds)
- Query InfluxDB
- Extract series, format as CSV, write file
- For each group in graph:
- For each period (or flat if null):
- For each graph in category:
-
Resource Collection (resourceCollector.js, if configured)
- Fetch Kubernetes pods from API
- Extract pod metadata (CPU limit, memory limit, restart count)
- Query Prometheus for each pod's CPU/Memory metrics
- Merge metrics with limits (e.g., CPU + CPU_Limit as columns)
- Write GeneralInfo.csv (pod specs + test timing + load config)
- Write Resources/*.csv (one per metric per pod)
Query Placeholders and Substitution:
InfluxQL queries in eporting/query_list.json contain:
- dateTimeStart: replaced with ISO string (e.g., "2026-03-25T18:49:00.123Z")
- dateTimeEnd: replaced with ISO string
- interval: replaced with "5s" or "15s" (per-group decision)
- CONV_MSG_CYCLES: replaced with numeric cycles (e.g., "6")
Example transformation:
-- Template:
SELECT SUM("value") / CONV_MSG_CYCLES FROM ... WHERE time > 'dateTimeStart' AND time < 'dateTimeEnd' GROUP BY time(interval)
-- After substitution (with interval=5s, cycles=6, times as below):
SELECT SUM("value") / 6 FROM ... WHERE time > '2026-03-25T18:49:00.123Z' AND time < '2026-03-25T18:51:00.456Z' GROUP BY time(5s)