How are raw performance metrics transformed into structured datasets?
This document explains the responsibilities of the reporting service, the processing flow after a test execution finishes, and how datasets are organized for further analysis.
The reporting service is a standalone Node.js application responsible for post-test processing.
Unlike k6, whose responsibility is generating workload and collecting runtime metrics, the reporting service focuses entirely on transforming collected data into persistent datasets.
Rather than generating HTML or PDF reports directly, it produces a structured collection of CSV datasets that can later be consumed by visualization tools, comparison utilities or custom report generators.
Keeping reporting independent from load generation allows both systems to evolve separately.
P.S. I do own a custom HTML report generator but it is out of scope for this project.
graph LR
subgraph Input["Input"]
Payload["HTTP Payload<br/>(POST /start-reporting)"]
end
subgraph Validation["Validation"]
Val["validateReportingRequest()"]
FieldCheck["Check required fields"]
ResourceCheck["Check resource config<br/>(all-or-nothing)"]
end
subgraph Orchestration["Orchestration"]
Orch["orchestrateReporting()"]
LoopCat["Loop each category<br/>(Aggregation, WebSocket, etc)"]
GetPeriods["getPeriodsForCategory()"]
end
subgraph InfluxCollection["InfluxDB Collection"]
InfluxOrch["collectMetricsByPeriod()"]
LoopGraph["Loop each graph"]
LoopGroup["Loop each group"]
IntervalDecide["Determine interval<br/>(group.name includes 'spread'?)"]
Substitute["substitutePlaceholders()<br/>(dateTimeStart, dateTimeEnd,<br/>interval, CONV_MSG_CYCLES)"]
Query["runInfluxQuery()"]
Extract["extractInfluxDBSeries()"]
Format["seriesToCsvLines()<br/>(include tags if transaction)"]
WriteCSV["Write CSV file"]
end
subgraph ResourceCollection["Resource Collection (Optional)"]
ResOrch["collectResourcesAndGeneralInfo()"]
FetchK8s["fetchPods()"]
ExtractPod["extractPodInfo()"]
QueryProm["queryPrometheusRange()<br/>(CPU, Memory, Limits)"]
MergeMetrics["mergeMetricsWithLimits()"]
WriteResCSV["Write Resources/*.csv"]
WriteGenInfo["Write GeneralInfo.csv"]
end
subgraph Output["Output"]
Files["CSV Files"]
end
Payload -->|1| Val
Val -->|2| FieldCheck
FieldCheck -->|3| ResourceCheck
ResourceCheck -->|Valid| Orch
Orch -->|4| LoopCat
LoopCat -->|5| GetPeriods
GetPeriods -->|6| InfluxOrch
InfluxOrch -->|7| LoopGraph
LoopGraph -->|8| LoopGroup
LoopGroup -->|9| IntervalDecide
IntervalDecide -->|10| Substitute
Substitute -->|11| Query
Query -->|12| Extract
Extract -->|13| Format
Format -->|14| WriteCSV
InfluxOrch -->|15| ResOrch
ResOrch -->|16| FetchK8s
FetchK8s -->|17| ExtractPod
ExtractPod -->|18| QueryProm
QueryProm -->|19| MergeMetrics
MergeMetrics -->|20| WriteResCSV
MergeMetrics -->|21| WriteGenInfo
WriteCSV -->|22| Files
WriteResCSV -->|22| Files
WriteGenInfo -->|22| Files
style Validation fill:#FFB6C1,stroke:#333
style Orchestration fill:#87CEEB,stroke:#333
style InfluxCollection fill:#90EE90,stroke:#333
style ResourceCollection fill:#FFD700,stroke:#333
style Output fill:#DDA0DD,stroke:#333
The reporting process begins during the teardown() phase of the k6 execution.
After all scenarios have finished:
- k6 determines the test time boundaries.
- A request containing test configuration and timestamps is sent to the reporting service.
- The reporting service validates the request.
- Metrics are collected from InfluxDB.
- Resource information is collected from Kubernetes and Prometheus (optional).
- Every collected dataset is formatted as CSV.
- CSV files are written to the output directory.
The reporting request is synchronous.
This means the k6 execution does not finish until reporting either succeeds or returns an error.
One of the core responsibilities of the reporting service is dividing a test into meaningful execution periods.
Instead of querying the entire execution as one continuous interval, three independent time windows are created.
- RampUp — from the beginning of the test until all virtual users have reached their target count.
- MaxLoad — from the end of the ramp-up until the end of the workload.
- WholeRun — the complete execution.
Different categories use different reporting strategies.
Aggregation and WebSocket datasets are generated separately for every period.
Metrics and Meta datasets describe the whole execution and therefore are generated only once.
Everything is not set in stone and easily configurable!
graph TD
QueryList["Query List<br/>(query_list.json)"]
QueryList -->|Category| Cat1["Aggregation"]
QueryList -->|Category| Cat2["WebSocket"]
QueryList -->|Category| Cat3["Metrics"]
QueryList -->|Category| Cat4["Meta"]
Cat1 -->|CATEGORY_PERIOD_MAP| Per1["[RampUp, MaxLoad, WholeRun]<br/>(3 directories)"]
Cat2 -->|CATEGORY_PERIOD_MAP| Per2["[RampUp, MaxLoad, WholeRun]<br/>(3 directories)"]
Cat3 -->|CATEGORY_PERIOD_MAP| Per3["null<br/>(flat, no periods)"]
Cat4 -->|CATEGORY_PERIOD_MAP| Per4["null<br/>(flat, no periods)"]
Per1 --> Window1["RampUp Window:<br/>test_start → test_maxload<br/>(VU ramp-up phase)"]
Per1 --> Window2["MaxLoad Window:<br/>test_maxload → test_end<br/>(steady-state phase)"]
Per1 --> Window3["WholeRun Window:<br/>test_start → test_end<br/>(entire test)"]
Window1 -->|InfluxDB Query| Query1["SELECT ... WHERE<br/>time > RampUp_start AND time < RampUp_end"]
Window2 -->|InfluxDB Query| Query2["SELECT ... WHERE<br/>time > MaxLoad_start AND time < MaxLoad_end"]
Window3 -->|InfluxDB Query| Query3["SELECT ... WHERE<br/>time > WholeRun_start AND time < WholeRun_end"]
Query1 -->|Results| CSV1["Aggregation/RampUp/graph.csv"]
Query2 -->|Results| CSV2["Aggregation/MaxLoad/graph.csv"]
Query3 -->|Results| CSV3["Aggregation/WholeRun/graph.csv"]
Per3 -->|Flat| FlatQ["SELECT ... WHERE<br/>time > test_start AND time < test_end"]
FlatQ -->|Results| FlatCSV["Metrics/graph.csv<br/>(no period subdirs)"]
style Cat1 fill:#FF6B6B,stroke:#333,color:#fff
style Cat2 fill:#4ECDC4,stroke:#333,color:#fff
style Cat3 fill:#45B7D1,stroke:#333,color:#fff
style Cat4 fill:#FFA07A,stroke:#333,color:#fff
style Per1 fill:#FF8C94,stroke:#333,color:#fff
style Per2 fill:#FF8C94,stroke:#333,color:#fff
style Per3 fill:#A8D8EA,stroke:#333,color:#fff
style Per4 fill:#FFB4A2,stroke:#333,color:#fff
The reporting service is configuration-driven.
It does not contain hardcoded InfluxDB queries.
Instead, every reporting category is described in reporting/query_list.json.
For every configured category the service:
- determines the reporting periods
- selects the appropriate aggregation interval
- substitutes query placeholders
- executes the InfluxDB query
- extracts returned series
- formats the result as CSV
The placeholders replaced during execution include:
- reporting time window
- aggregation interval
- conversation cycle count (for messaging)
This allows the same query template to be reused for different execution periods without duplicating query definitions.
Infrastructure metrics are collected independently from application metrics.
If Kubernetes and Prometheus are configured, the reporting service:
- retrieves pod metadata from Kubernetes
- discovers running pods on namespace
- queries Prometheus for resource usage
- combines usage metrics with configured resource limits
- writes the resulting datasets alongside application metrics
If infrastructure collection fails, reporting continues and application datasets are still generated.
Infrastructure metrics are treated as an optional extension rather than a mandatory dependency
Every test execution produces its own directory identified by the build tag.
results/<BUILD_TAG>/
├── Aggregation/
│ ├── RampUp/
│ │ ├── Aggregation data.csv
│ │ ├── Aggregation by interval data.csv
│ │ ├── Aggregation by transaction.csv
│ │ ├── Aggregation data spread.csv
│ │ └── Aggregation by interval data spread.csv
│ ├── MaxLoad/
│ │ └── (same files as RampUp)
│ └── WholeRun/
│ └── (same files as RampUp)
├── WebSocket/
│ ├── RampUp/
│ │ ├── WebSocket Metrics.csv
│ │ └── WebSocket Counters.csv
│ ├── MaxLoad/
│ │ └── (same files as RampUp)
│ └── WholeRun/
│ └── (same files as RampUp)
├── Metrics/
│ ├── Database requests AND Create Agent.csv
│ └── ... (per logical metric group)
├── Meta/
│ ├── Agent Activity.csv
│ ├── Database Connections.csv
│ └── Conversations by status.csv
├── Resources/
│ ├── myapp-abc123_CPU.csv
│ ├── myapp-abc123_Memory.csv
│ └── ... (per pod, per metric)
└── GeneralInfo.csv
Every directory represents one logical category of collected information.
This organization allows individual datasets to evolve independently while remaining easy to locate during post-test analysis.
Provides statistical summaries of collected measurements.
Depending on the dataset, information is grouped:
- by execution period
- by transaction
- by aggregation interval
- by spread (variability)
These datasets are intended to quickly identify bottlenecks before investigating detailed time-series data.
Contains metrics describing asynchronous communication.
Separate datasets are generated for every execution period, making it possible to compare WebSocket behaviour during ramp-up and steady-state execution.
Contains grouped application metrics collected throughout the test.
Unlike Aggregation datasets, these files preserve logical groupings defined in the query configuration and are not divided into execution periods.
Meta (or Business Metrics) datasets describe the application behaviour under test.
Examples include:
- agent activity (utilization)
- database connections
- conversations by state
These datasets are useful for validating that the application business logic behaved as expected.
Infrastructure datasets combine Kubernetes metadata with Prometheus measurements.
Each pod produces its own resource files, allowing infrastructure behaviour to be analysed independently of application metrics.
Contains metadata describing the execution.
Examples include:
- test timings
- workload configuration
- pod specifications
- resource limits
This file provides the context required to correctly interpret all generated datasets.
The reporting pipeline was designed to be configuration-driven.
Most reporting extensions require changes only to the query configuration.
Typical examples include:
- adding a new metric category
as simeple as adding 1 line to
reporting/query_list.json
- introducing another graph
as simeple as adding 2 lines - name and InfluxQL query - to
reporting/query_list.json
- extending resource collection
as simple as adding 2 lines — metric name and PromQL query — to
getResourceMetricQueries()
- adding another reporting period
define time window in
collectMetricsByPeriod()wire it up to categories in
getPeriodsForCategory()
Because orchestration, querying and CSV generation are separated into independent modules, new datasets can usually be introduced without modifying existing reporting logic.
Moreover, the code is heavily documented.
I can claim that even a person who never coded would understand what is what and how it works!