Skip to content

Latest commit

 

History

History
759 lines (547 loc) · 28.5 KB

File metadata and controls

759 lines (547 loc) · 28.5 KB

Architecture

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.


Introduction

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.


High-Level Architecture

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.

Diagram: System Architecture (High-Level)

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
Loading

Design Principles

Before discussing individual components, it is worth explaining the principles that guided the architecture.

Single Responsibility

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.


Observability First

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.


Explicit Communication

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.


Distribution-Friendly Design

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.


Component Overview

Distributed Execution Architecture

The framework supports seamless scaling from single-worker to multi-worker execution through an elegant single-sender monitor architecture.

Multi-Worker Coordination Pattern

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.

Monitor Election Mechanism

At setup completion (each worker runs setup independently):

  1. All workers initialize their monitor state as "blocked" in Redis: monitor_workers_state:{BUILD_TAG}
  2. Each worker registers itself as "working" in: reporting_workers_state:{BUILD_TAG}
  3. At setup end, if ALL workers are blocked, one worker is randomly elected to become "unblocked"
  4. 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"
}

What Primary Monitor Does

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

What Blocked Monitors Do

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

Test Window Coordination

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 ✓

Reporting Synchronization

When workers complete teardown:

  1. Each worker marks itself as "done" in Redis: reporting_workers_state:{BUILD_TAG}
  2. Each worker calls POST /start-reporting (non-blocking)
  3. Reporting Service waits for ALL workers to mark "done" OR times out after 60 seconds
  4. When all workers done, service triggers reporting ONCE
  5. 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)

Benefits

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

Deployment Flexibility

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

Component Overview

Load Generation

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.


Synchronization Layer

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:

VU synchronization


Metrics Layer

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:

HTTP metrics aren't enough


Reporting Layer

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:

k6 limitations for reporting


Visualization Layer

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.


Extensibility

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.


Lessons Learned

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.


Architecture Layers

Layer 1: k6 Load Test (Synchronous VU Execution)

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:

  1. 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)
  2. 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:
  3. 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)
  4. 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
  5. 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
  6. 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

Layer 2: State Coordination (Redis)

Redis serves dual purposes:

  1. Target under test (agents read notifications, customers see updates)
  2. Cross-VU coordination mechanism

Redis Keys Used (organized by purpose):

Load Coordination (Per-Worker)

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

Load Coordination (Global)

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)

WebSocket Coordination

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

Test Window Coordination (Min/Max Semantics)

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

Worker State Coordination (Distributed Mode)

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.

Layer 3: Metrics Streaming (Push to InfluxDB)

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)

Layer 4: Reporting Service (Node.js Express)

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:

  1. 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
  2. Orchestration (reportingOrchestrator.js)

    • Loop each category in queryList.types
    • Determine period strategy: CATEGORY_PERIOD_MAP
    • Call collectMetricsByPeriod() with category-specific rules
  3. 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
  4. 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)