Export Zathras benchmark results to OpenSearch for centralized analysis, dashboards, and performance tracking. Horreum export is available as a stub (not implemented) for future use.
Quick start: For a local Python workflow, follow Installation & Setup first (venv, install, verify, optional tests, then Configuration). Then use How to run for CLI examples. Prefer Running with a container? Skip pip there; you still supply config and volume mounts. Deeper topics (benchmark matrix, CI, OpenSearch queries) come later in the document.
Complete this section on your machine before How to run (unless you use only the container image).
- Python 3.9+
- Zathras benchmark results
- OpenSearch access (optional if you only use
--output-json)
Use a project virtual environment; do not install into the system Python.
cd /path/to/chronicler
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# OpenSearch export (most common):
pip install ".[opensearch]"
# JSON-only output (no OpenSearch Python client):
pip install .
# Development (editable install, tests, and OpenSearch client):
pip install -e ".[opensearch,dev]"Verify the install:
python3 -c "import chronicler; print(chronicler.__file__)"Dependencies and optional extras are defined in pyproject.toml. Prefer the commands above over requirements.txt (kept as a legacy flat list; see comments in that file).
Running tests (with venv activated):
cd /path/to/chronicler
pip install -e ".[opensearch,dev]" # if not already installed for development
pytest tests/ -v # Run full suite (209 tests)
# Run by category
pytest -m unit # Fast unit tests only (178 tests)
pytest -m integration # Integration tests with file I/O (20 tests)The test suite covers:
- Schema: dataclass serialization, validation, content hashing
- Utilities: timestamp validation, parsing, run helpers
- Archive handling: zip/tar extraction
- Processors: timestamp validation, run parsing, metric extraction
Dependencies (see pyproject.toml):
pyyaml— configuration and result parsing (always installed)opensearch-py— OpenSearch export (install with.[opensearch])requests— optional Horreum client path (install with.[horreum]; Horreum export is still a stub)
Create your configuration file next to the Chronicler Python package (so it is found without passing --config):
PKG_CONFIG=$(python3 -c "from pathlib import Path; import chronicler; print(Path(chronicler.__file__).parent/'config')")
cp "$PKG_CONFIG/export_config_example.yml" "$PKG_CONFIG/export_config.yml"
vim "$PKG_CONFIG/export_config.yml"Discovery order (when --config is omitted):
CHRONICLER_CONFIG— absolute or relative path to a YAML file (recommended for CI/orchestrators).- Package path —
<chronicler>/config/export_config.yml(same directory asexport_config_example.ymlin the install). - Current working directory, first match:
./export_config.yml→./config/export_config.yml→./chronicler/config/export_config.yml.
If no file is found, the CLI logs a warning and runs with an empty config (OpenSearch code defaults only). Passing --config PATH always uses that path and overrides discovery. Each run logs which file is used (or that the explicit path is missing).
Orchestrators (e.g. Zathras) can rely on CHRONICLER_CONFIG or on a pre-installed export_config.yml beside the package, avoiding a hard-coded path to the Chronicler source tree.
Example config:
opensearch:
url: "https://opensearch.example.com"
summary_index: "zathras-results" # Summary documents
timeseries_index: "zathras-timeseries" # Individual time series points
username: "example-user"
password: "your-password"
verify_ssl: false # Set to true for production
# Horreum export is not implemented (stub only)
horreum:
url: "http://localhost:8080"
username: "your-horreum-username"
password: "your-horreum-password"
test_name: "Zathras Benchmarks"
processing:
batch_size: 500
continue_on_error: true
verbose: falseAfter completing Installation & Setup above (including Install the package and Configuration), run the post-processing CLI on your result directories (same activated venv you used for pip install):
# Process and export (config is discovered automatically; see Configuration section)
python3 -m chronicler.run_postprocessing \
--input /path/to/results \
--opensearch
# Or just generate JSON files
python3 -m chronicler.run_postprocessing \
--input /path/to/results \
--output-json /tmp/json_outputWhat it does:
- Recursively discovers all result directories
- Auto-detects benchmark types (coremark, streams, pyperf, etc.)
- Batch processes all results
- Exports to OpenSearch or saves JSON (Horreum is a stub)
- Provides detailed summary report
Example output:
2025-11-06 21:00:31 - Searching for results in: production_data
2025-11-06 21:00:31 - Found 34 result directory(ies)
2025-11-06 21:00:34 - Processing coremark: results_coremark.zip
[SUCCESS] Parsed coremark: coremark_Standard_D128ds_v6_2_20251107_020034
[EXPORT] Exported to OpenSearch (summary): coremark_Standard_D128ds_v6_2_20251107_020034
======================================================================
PROCESSING SUMMARY
======================================================================
Total: 109 files
Successful: 78 benchmarks
Skipped: 31 (unknown types)
Duration: 109.38 seconds
Tests Processed:
- coremark: 12
- streams: 11
- pyperf: 6 (34,080 time series points)
- specjbb: 11
- And more...
Use this path when you prefer not to install Chronicler with pip locally. The image includes the CLI entrypoint; you still provide a config file and mount your result directories.
A pre-built image is available at quay.io/zathras/chronicler.
The container expects the config file at /config/config.yaml (set via CHRONICLER_CONFIG). Copy and edit the example:
cp config/export_config_example.yml /path/to/your/config.yaml
vim /path/to/your/config.yaml # Add your credentialsMount your config file and your benchmark results directory into the container, then pass the same CLI flags as the local invocation:
# Podman — export to OpenSearch
podman run --rm \
-v /path/to/your/config.yaml:/config/config.yaml:z \
-v /path/to/results:/results:z \
quay.io/zathras/chronicler \
--input /results \
--opensearch
# Docker — export to OpenSearch
docker run --rm \
-v /path/to/your/config.yaml:/config/config.yaml \
-v /path/to/results:/results \
quay.io/zathras/chronicler \
--input /results \
--opensearch
# Write JSON output instead of exporting
podman run --rm \
-v /path/to/results:/results:z \
-v /tmp/json_output:/output:z \
quay.io/zathras/chronicler \
--input /results \
--output-json /outputNote: The
:zflag on volume mounts is required by SELinux on Fedora/RHEL hosts. Omit it on other platforms.
If you prefer to place the config file elsewhere, override the environment variable:
podman run --rm \
-e CHRONICLER_CONFIG=/data/my_config.yaml \
-v /path/to/your/config.yaml:/data/my_config.yaml:z \
-v /path/to/results:/results:z \
quay.io/zathras/chronicler \
--input /results \
--opensearch| Benchmark | Post-Processing Support | Processor | Notes |
|---|---|---|---|
| CoreMark | Supported | coremark_processor.py |
Single-thread CPU performance |
| CoreMark Pro | Supported | coremark_pro_processor.py |
9 workload types |
| FIO | Supported | fio_processor.py |
Flexible I/O tester |
| HPL (autohpl) | Supported | autohpl_processor.py |
High Performance Computing Linpack |
| Passmark | Supported | passmark_processor.py |
CPU & Memory marks |
| Phoronix Test Suite | Supported | phoronix_processor.py |
51 sub-tests (BOPs) |
| Pig | Supported | pig_processor.py |
Processor scheduler efficiency |
| PyPerf | Supported | pyperf_processor.py |
90 Python benchmarks, 5,680 time series points |
| SPEC CPU 2017 | Supported | speccpu2017_processor.py |
Compute-intensive performance suite |
| SpecJBB | Supported | specjbb_processor.py |
Java business benchmark (Critical/Max-jOPS) |
| STREAM | Supported | streams_processor.py |
Memory bandwidth (Copy, Scale, Add, Triad) |
| Uperf | Supported | uperf_processor.py |
Network performance (IOPS, latency, throughput) |
| HammerDB | Not Supported | - | Database benchmarking (MariaDB, PostgreSQL) |
| IOzone | Not Supported | - | File system benchmarking |
| Linpack | Not Supported | - | Licensed Linpack benchmark |
| NUMA STREAM | Not Supported | - | NUMA memory bandwidth extension |
12 of 16 benchmarks supported | 35,000+ time series points per production run
Burden has built-in Chronicler integration. No script modifications needed.
Setup:
-
Install Chronicler with OpenSearch support:
pip install 'chronicler[opensearch]' -
Configure export credentials (choose one method):
# Option 1: Environment variable (recommended for CI/orchestrators) export CHRONICLER_CONFIG=/path/to/export_config.yml # Option 2: Package config directory (standard location) PKG_CONFIG=$(python3 -c "from pathlib import Path; import chronicler; print(Path(chronicler.__file__).parent/'config')") cp "$PKG_CONFIG/export_config_example.yml" "$PKG_CONFIG/export_config.yml" vim "$PKG_CONFIG/export_config.yml" # Add your credentials # Option 3: Zathras config directory # Place export_config.yml in the Zathras top-level config/ directory cd /path/to/zathras cp /path/to/chronicler/config/export_config_example.yml config/export_config.yml vim config/export_config.yml # Add your credentials
Config is discovered automatically in this order:
CHRONICLER_CONFIGenvironment variable, packageexport_config.yml, Zathrasconfig/export_config.yml, or legacy../chronicler/config/export_config.yml.
Usage:
# Standard mode (continues on Chronicler errors):
burden --run_chronicler [other burden options]
# Strict mode (aborts burden if Chronicler fails):
burden --run_chronicler_strict [other burden options]
# Alternatively, use environment variable with --run_chronicler:
CHRONICLER_STRICT=1 burden --run_chronicler [other burden options]
# Verbose Chronicler output:
CHRONICLER_VERBOSE=1 burden --run_chronicler [other burden options]
# OR
burden --run_chronicler --ansible_noise_level dense [other burden options]Benefits:
- Zero manual script modification
- Results exported automatically after test completion
- Graceful failure handling with
--run_chronicleror strict abort with--run_chronicler_strict - Automatic config discovery from multiple locations
- Works with both archived and non-archived results
- Verbose logging available for troubleshooting
Access your exported data:
- Discover: https://your-opensearch-url/_dashboards/app/discover
- Dev Tools: https://your-opensearch-url/_dashboards/app/dev_tools
# Get latest results
GET /zathras-results/_search
{
"size": 10,
"sort": [{ "metadata.test_timestamp": "desc" }]
}
# Find CoreMark results
GET /zathras-results/_search
{
"query": { "term": { "test.name": "coremark" }}
}
# Performance over time (summary data)
GET /zathras-results/_search
{
"query": { "term": { "test.name": "coremark" }},
"sort": [{ "metadata.test_timestamp": "asc" }],
"_source": ["metadata.test_timestamp", "results.runs.run_0.mean"]
}
# Get time series data points
GET /zathras-timeseries/_search
{
"query": {
"bool": {
"must": [
{ "term": { "test.name": "pyperf" }},
{ "term": { "results.run.benchmark_name": "2to3" }}
]
}
},
"sort": [{ "metadata.sequence": "asc" }]
}
# Compare by CPU architecture
GET /zathras-results/_search
{
"size": 0,
"aggs": {
"by_arch": {
"terms": { "field": "system_under_test.hardware.cpu.architecture.keyword" },
"aggs": {
"avg_performance": { "avg": { "field": "results.runs.run_0.mean" }}
}
}
}
}The post-processing pipeline automatically extracts and structures data from Zathras result files:
- Performance metrics: Mean, median, min, max, standard deviation for each run
- Time series data: Individual data points with sequence ordering (for benchmarks like PyPerf)
- Configuration details: Test-specific settings and parameters
- Execution metadata: Start time, duration, status (PASS/FAIL)
- Validation data: Checksums, compiler information
Hardware:
- CPU: Vendor, model, architecture, cores, threads, cache sizes, CPU flags (as boolean objects)
- Memory: Total capacity, speed, NUMA topology (node-based objects)
- Storage: Devices, capacity, types, mount points
- Network: Interfaces, speeds, addresses
Operating System:
- Distribution, version, kernel version
- Tuned profile settings
- Sysctl parameters
- SELinux configuration
- Test parameters and iteration counts
- System tuning settings applied during test
- Zathras scenario information
Automatically parsed from result directory paths:
- OS Vendor: e.g.,
rhel,ubuntu,fedora - Cloud Provider: e.g.,
azure,aws,gcp,local - Instance Type: e.g.,
Standard_D8ds_v6,m5.xlarge - Iteration Number: e.g.,
0,1,2 - Scenario Name: e.g.,
az_rhel_10_ga
Example directory structure: production_data/az_rhel_10_ga/rhel/azure/Standard_D8ds_v6_1/
Zathras uses two OpenSearch indices to handle high-volume time series data:
zathras-results- Summary documents with aggregated statistics (mean, median, stdev, etc.)zathras-timeseries- Individual time series data points (one document per point)
Why two indices? Benchmarks like PyPerf generate 5,680+ time series points per test, exceeding OpenSearch's 5,000 field limit for a single document. The two-index approach keeps summaries queryable while preserving full time series data.
python3 -m chronicler.run_postprocessing \
--input /path/to/results \
--opensearch
# Optional: --config /path/to/export_config.yml overrides discoveryHorreum export is not implemented. The HorreumExporter class is a stub: it accepts the same constructor and method calls for compatibility, but export_zathras_document (and other methods) raise NotImplementedError. Implement chronicler/exporters/horreum_exporter.py to enable Horreum.
from chronicler.exporters.horreum_exporter import HorreumExporter
# Stub accepts url, username, password (and **kwargs for future use)
exporter = HorreumExporter(
url="https://horreum.example.com",
username="user",
password="pass"
)
# exporter.export_zathras_document(document) # Raises NotImplementedErrorZathras uses content-based checksums to prevent duplicate documents when reprocessing the same test results.
Each document is assigned a deterministic ID based on a SHA256 hash of its content:
Document ID = {test_name}_{content_hash[:16]}
Example: coremark_fdcfbbf0e6a525ea
The hash includes:
- Test name, version, and configuration
- System details (CPU, memory, OS)
- All benchmark results and metrics
- Original test timestamp
The hash excludes processing_timestamp, ensuring identical test results always generate the same ID.
Prevents Duplicates:
# Process results
python3 -m chronicler.run_postprocessing --input results/ --opensearch
# Reprocess same results (e.g., after fixing a bug)
python3 -m chronicler.run_postprocessing --input results/ --opensearch
# Result: Same document updated in OpenSearch, no duplicate createdSafe Reprocessing:
- Fix processor bugs without creating duplicates
- Add new fields to existing documents
- Update metadata or extractors
- Track last processing time via
processing_timestamp
OpenSearch Behavior:
# First upload
POST /zathras-results/_doc/coremark_fdcfbbf0e6a525ea
→ Creates new document
# Second upload (same test results, different processing time)
POST /zathras-results/_doc/coremark_fdcfbbf0e6a525ea
→ Updates existing document (no duplicate)Duplicates only happen when actual test data differs:
- Different test runs (different timestamps/results)
- Different systems (different CPU/memory)
- Different configurations (different parameters)
This is expected behavior representing genuinely different tests.
Deduplication is built-in and automatic. To verify it's working, process the same results twice and query OpenSearch - you'll see only one document exists with an updated processing_timestamp.
Zathras uses two separate OpenSearch indices to efficiently handle benchmarks with large time series datasets:
1. zathras-results - Summary Documents
Contains aggregated statistics for each benchmark execution:
{
"metadata": {
"document_id": "coremark_Standard_D8ds_v6_1_20251106",
"test_timestamp": "2025-11-06T12:00:00Z",
"processing_timestamp": "2025-11-06T12:05:00Z",
"os_vendor": "rhel",
"cloud_provider": "azure",
"instance_type": "Standard_D8ds_v6",
"iteration": 1,
"scenario_name": "az_rhel_10_ga"
},
"test": {
"name": "coremark",
"version": "1.0"
},
"system_under_test": {
"hardware": {
"cpu": {
"vendor": "Intel",
"model": "Xeon Platinum 8370C",
"architecture": "x86_64",
"cores": 8,
"threads": 16,
"flags": { "avx2": true, "avx512": true, "sse4_2": true }
},
"memory": {
"total_gb": 32,
"speed_mhz": 3200
}
},
"os": {
"vendor": "rhel",
"version": "10.0",
"kernel": "6.11.0-0.rc5.20240828git6a0e38f.45.el10.x86_64"
}
},
"results": {
"runs": {
"run_0": {
"status": "PASS",
"mean": 193245.2,
"median": 193500.0,
"stdev": 1234.5,
"min": 191000.0,
"max": 195000.0
}
}
}
}2. zathras-timeseries - Individual Time Series Points
Stores each time series data point as a separate, fully denormalized document:
{
"metadata": {
"document_id": "pyperf_Standard_D8ds_v6_1_20251106",
"timeseries_id": "pyperf_Standard_D8ds_v6_1_20251106_run0_2to3_seq0",
"timestamp": "2025-11-06T12:00:00Z",
"sequence": 0,
"test_timestamp": "2025-11-06T12:00:00Z",
"processing_timestamp": "2025-11-06T12:05:00Z",
"os_vendor": "rhel",
"cloud_provider": "azure",
"instance_type": "Standard_D8ds_v6",
"iteration": 1
},
"test": {
"name": "pyperf",
"version": "1.0"
},
"system_under_test": {
/* Full SUT details included */
},
"results": {
"run": {
"run_key": "run_0",
"run_number": 0,
"status": "PASS",
"benchmark_name": "2to3"
},
"value": 1.23,
"unit": "seconds"
}
}Fully Denormalized
- Each document contains complete context (test, SUT, configuration)
- No joins required for querying
- Optimized for document-oriented datastores
Object-Based Structure
- Dynamic keys like
run_0,node_0,device_0instead of arrays - Better OpenSearch performance for aggregations
- Avoids nested object limitations
Hierarchical Metadata
- Structured extraction from directory paths
- Consistent field naming across indices
- Enables filtering by cloud provider, instance type, iteration
Dual Timestamps
test_timestamp: When the benchmark was executedprocessing_timestamp: When the JSON document was created- Enables tracking of both test execution and data pipeline timing
Boolean CPU Flags
- CPU features represented as objects:
{"avx2": true, "avx512": false} - Efficient term queries for hardware capability filtering
GET /zathras-results/_search
{
"query": {
"bool": {
"must": [
{ "term": { "test.name": "coremark" }},
{ "range": { "metadata.test_timestamp": { "gte": "now-7d" }}}
]
}
},
"sort": [{ "metadata.test_timestamp": "asc" }],
"_source": ["metadata.test_timestamp", "results.runs.run_0.mean", "metadata.instance_type"]
}GET /zathras-results/_search
{
"query": { "term": { "test.name": "coremark" }},
"aggs": {
"by_cpu_model": {
"terms": { "field": "system_under_test.hardware.cpu.model.keyword", "size": 10 },
"aggs": {
"avg_performance": { "avg": { "field": "results.runs.run_0.mean" }},
"max_performance": { "max": { "field": "results.runs.run_0.max" }}
}
}
}
}# Systems with AVX-512 and more than 64 cores
GET /zathras-results/_search
{
"query": {
"bool": {
"must": [
{ "term": { "system_under_test.hardware.cpu.flags.avx512": true }},
{ "range": { "system_under_test.hardware.cpu.cores": { "gt": 64 }}}
]
}
},
"_source": ["system_under_test.hardware.cpu", "metadata.instance_type", "results.runs.run_0.mean"]
}# Get all time series points for a specific benchmark run
GET /zathras-timeseries/_search
{
"query": {
"bool": {
"must": [
{ "term": { "metadata.document_id": "pyperf_Standard_D8ds_v6_1_20251107" }},
{ "term": { "results.run.benchmark_name": "2to3" }}
]
}
},
"sort": [{ "metadata.sequence": "asc" }],
"size": 100
}
# Aggregate time series data
GET /zathras-timeseries/_search
{
"query": { "term": { "test.name": "pyperf" }},
"aggs": {
"by_benchmark": {
"terms": { "field": "results.run.benchmark_name.keyword" },
"aggs": {
"avg_value": { "avg": { "field": "results.value" }},
"min_value": { "min": { "field": "results.value" }},
"max_value": { "max": { "field": "results.value" }}
}
}
}
}- Index Template - OpenSearch mappings
- Config README - OpenSearch connection and index templates
- Processors README - Processor architecture and adding new benchmarks
- OpenSearch Query DSL
- OpenSearch Aggregations
- Horreum Documentation (for future implementation)