001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.benchmarking.common.metrics; 017 018import java.time.Duration; 019import java.time.Instant; 020import java.util.*; 021 022/** 023 * Transforms Prometheus time-series data into the benchmark server metrics format 024 * as defined in benchmark-metrics.adoc. 025 * 026 */ 027public class BenchmarkMetricsTransformer { 028 029 private static final String METRIC_PROCESS_CPU_USAGE = "process_cpu_usage"; 030 private static final String METRIC_JVM_MEMORY_USED_BYTES = "jvm_memory_used_bytes"; 031 032 /** 033 * Transforms Prometheus time-series data into benchmark server metrics. 034 * 035 * @param benchmarkName Name of the benchmark 036 * @param startTime Start time of the benchmark 037 * @param endTime End time of the benchmark 038 * @param timeSeriesData Raw time-series data from Prometheus 039 * @return Structured benchmark metrics as defined in requirements 040 */ 041 public Map<String, Object> transformToServerMetrics( 042 String benchmarkName, 043 Instant startTime, 044 Instant endTime, 045 Map<String, PrometheusClient.TimeSeries> timeSeriesData) { 046 047 Map<String, Object> result = new LinkedHashMap<>(); 048 Duration duration = Duration.between(startTime, endTime); 049 050 // Benchmark metadata 051 Map<String, Object> benchmark = new LinkedHashMap<>(); 052 benchmark.put("name", benchmarkName); 053 benchmark.put("start_time", startTime.toString()); 054 benchmark.put("end_time", endTime.toString()); 055 benchmark.put("duration_seconds", duration.getSeconds()); 056 result.put("benchmark", benchmark); 057 058 // Resource metrics 059 result.put("resources", createResourceMetrics(timeSeriesData)); 060 061 // Application metrics 062 result.put("application", createApplicationMetrics(timeSeriesData)); 063 064 return result; 065 } 066 067 068 private Map<String, Object> createResourceMetrics(Map<String, PrometheusClient.TimeSeries> data) { 069 Map<String, Object> resources = new LinkedHashMap<>(); 070 071 // CPU metrics 072 Map<String, Object> cpu = new LinkedHashMap<>(); 073 cpu.put("process", createCpuMetrics(data.get(METRIC_PROCESS_CPU_USAGE), "Process")); 074 cpu.put("system", createCpuMetrics(data.get("system_cpu_usage"), "System")); 075 076 // Get CPU cores 077 PrometheusClient.TimeSeries cpuCount = data.get("system_cpu_count"); 078 if (cpuCount != null && !cpuCount.values().isEmpty()) { 079 cpu.put("cores_available", (int) cpuCount.values().getFirst().value()); 080 } else { 081 cpu.put("cores_available", 4); // Default 082 } 083 resources.put("cpu", cpu); 084 085 // Memory metrics 086 resources.put("memory", createMemoryMetrics(data)); 087 088 // Thread metrics 089 resources.put("threads", createThreadMetrics(data)); 090 091 return resources; 092 } 093 094 private Map<String, Object> createCpuMetrics(PrometheusClient.TimeSeries series, String type) { 095 Map<String, Object> cpuMetrics = new LinkedHashMap<>(); 096 097 if (series == null || series.values().isEmpty()) { 098 cpuMetrics.put("average_percent", 0.0); 099 cpuMetrics.put("peak_percent", 0.0); 100 cpuMetrics.put("std_dev", 0.0); 101 return cpuMetrics; 102 } 103 104 List<Double> values = series.values().stream() 105 .map(dp -> dp.value() * 100) // Convert to percentage 106 .toList(); 107 108 Statistics stats = calculateStatistics(values); 109 110 cpuMetrics.put("average_percent", round(stats.mean, 1)); 111 cpuMetrics.put("peak_percent", round(stats.max, 1)); 112 cpuMetrics.put("std_dev", round(stats.stdDev, 2)); 113 114 if ("Process".equals(type)) { 115 Map<String, Object> percentiles = new LinkedHashMap<>(); 116 percentiles.put("p50", round(stats.p50, 1)); 117 percentiles.put("p75", round(stats.p75, 1)); 118 percentiles.put("p90", round(stats.p90, 1)); 119 percentiles.put("p99", round(stats.p99, 1)); 120 cpuMetrics.put("percentiles", percentiles); 121 } 122 123 return cpuMetrics; 124 } 125 126 private Map<String, Object> createMemoryMetrics(Map<String, PrometheusClient.TimeSeries> data) { 127 Map<String, Object> memory = new LinkedHashMap<>(); 128 129 // Heap memory 130 Map<String, Object> heap = new LinkedHashMap<>(); 131 PrometheusClient.TimeSeries heapUsed = findHeapMemory(data.get(METRIC_JVM_MEMORY_USED_BYTES)); 132 if (heapUsed != null && !heapUsed.values().isEmpty()) { 133 List<Double> heapMbValues = heapUsed.values().stream() 134 .map(dp -> dp.value() / 1024 / 1024) // Convert to MB 135 .toList(); 136 137 Statistics stats = calculateStatistics(heapMbValues); 138 heap.put("average_mb", round(stats.mean, 1)); 139 heap.put("peak_mb", round(stats.max, 1)); 140 heap.put("final_mb", round(heapMbValues.getLast(), 1)); 141 } else { 142 heap.put("average_mb", 0.0); 143 heap.put("peak_mb", 0.0); 144 heap.put("final_mb", 0.0); 145 } 146 memory.put("heap", heap); 147 148 // GC metrics 149 Map<String, Object> gc = new LinkedHashMap<>(); 150 PrometheusClient.TimeSeries gcOverhead = data.get("jvm_gc_overhead"); 151 if (gcOverhead != null && !gcOverhead.values().isEmpty()) { 152 double avgOverhead = gcOverhead.values().stream() 153 .mapToDouble(PrometheusClient.DataPoint::value) 154 .average().orElse(0.0); 155 gc.put("overhead_percent", round(avgOverhead * 100, 2)); 156 } else { 157 gc.put("overhead_percent", 0.0); 158 } 159 memory.put("gc", gc); 160 161 return memory; 162 } 163 164 private Map<String, Object> createThreadMetrics(Map<String, PrometheusClient.TimeSeries> data) { 165 Map<String, Object> threads = new LinkedHashMap<>(); 166 167 PrometheusClient.TimeSeries liveThreads = data.get("jvm_threads_live_threads"); 168 if (liveThreads != null && !liveThreads.values().isEmpty()) { 169 Statistics stats = calculateStatistics( 170 liveThreads.values().stream() 171 .map(PrometheusClient.DataPoint::value) 172 .toList() 173 ); 174 threads.put("average", (int) stats.mean); 175 threads.put("peak", (int) stats.max); 176 threads.put("final", (int) liveThreads.values().getLast().value()); 177 } else { 178 threads.put("average", 0); 179 threads.put("peak", 0); 180 threads.put("final", 0); 181 } 182 183 PrometheusClient.TimeSeries daemonThreads = data.get("jvm_threads_daemon_threads"); 184 if (daemonThreads != null && !daemonThreads.values().isEmpty()) { 185 threads.put("daemon", (int) daemonThreads.values().getFirst().value()); 186 } else { 187 threads.put("daemon", 0); 188 } 189 190 return threads; 191 } 192 193 194 private Map<String, Object> createApplicationMetrics(Map<String, PrometheusClient.TimeSeries> data) { 195 Map<String, Object> application = new LinkedHashMap<>(); 196 197 Map<String, Object> jwtValidations = new LinkedHashMap<>(); 198 199 // JWT validation metrics 200 PrometheusClient.TimeSeries jwtSuccess = data.get("sheriff_oauth_validation_success_operations_total"); 201 double totalSuccess = 0; 202 double cacheHits = 0; 203 204 if (jwtSuccess != null && !jwtSuccess.values().isEmpty()) { 205 totalSuccess = getDeltaValue(jwtSuccess); 206 // Check if it's a cache hit based on metric labels 207 if (jwtSuccess.labels().containsKey("event_type") && 208 jwtSuccess.labels().get("event_type").contains("CACHE_HIT")) { 209 cacheHits = totalSuccess; 210 } 211 } 212 213 PrometheusClient.TimeSeries jwtErrors = data.get("sheriff_oauth_validation_errors_total"); 214 double totalErrors = jwtErrors != null ? getDeltaValue(jwtErrors) : 0; 215 216 double total = totalSuccess + totalErrors; 217 jwtValidations.put("total", (int) total); 218 jwtValidations.put("success", (int) totalSuccess); 219 jwtValidations.put("errors", (int) totalErrors); 220 jwtValidations.put("cache_hits", (int) cacheHits); 221 jwtValidations.put("cache_hit_rate_percent", total > 0 ? round(cacheHits / total * 100, 1) : 0.0); 222 223 // JWT validation timing 224 PrometheusClient.TimeSeries jwtDurationSum = data.get("sheriff_oauth_bearer_token_validation_seconds_sum"); 225 PrometheusClient.TimeSeries jwtDurationCount = data.get("sheriff_oauth_bearer_token_validation_seconds_count"); 226 if (jwtDurationSum != null && jwtDurationCount != null) { 227 double sum = getDeltaValue(jwtDurationSum); 228 double count = getDeltaValue(jwtDurationCount); 229 if (count > 0) { 230 jwtValidations.put("average_validation_time_ms", round(sum / count * 1000, 2)); 231 } 232 } 233 234 application.put("jwt_validations", jwtValidations); 235 236 return application; 237 } 238 239 private PrometheusClient.TimeSeries findHeapMemory(PrometheusClient.TimeSeries memorySeries) { 240 if (memorySeries == null) { 241 return null; 242 } 243 // Check if this is heap memory based on labels 244 if (memorySeries.labels().containsKey("area") && 245 "heap".equals(memorySeries.labels().get("area"))) { 246 return memorySeries; 247 } 248 return null; 249 } 250 251 private double getDeltaValue(PrometheusClient.TimeSeries series) { 252 if (series == null || series.values().isEmpty()) { 253 return 0; 254 } 255 List<PrometheusClient.DataPoint> values = series.values(); 256 if (values.size() == 1) { 257 return values.getFirst().value(); 258 } 259 // For counters, get the difference between last and first 260 return values.getLast().value() - values.getFirst().value(); 261 } 262 263 private Statistics calculateStatistics(List<Double> values) { 264 if (values.isEmpty()) { 265 return new Statistics(); 266 } 267 268 List<Double> sorted = new ArrayList<>(values); 269 Collections.sort(sorted); 270 271 Statistics stats = new Statistics(); 272 stats.mean = values.stream().mapToDouble(Double::doubleValue).average().orElse(0); 273 stats.min = sorted.getFirst(); 274 stats.max = sorted.getLast(); 275 276 if (values.size() > 1) { 277 double variance = values.stream() 278 .mapToDouble(v -> Math.pow(v - stats.mean, 2)) 279 .average().orElse(0); 280 stats.stdDev = Math.sqrt(variance); 281 } 282 283 stats.p50 = getPercentile(sorted, 50); 284 stats.p75 = getPercentile(sorted, 75); 285 stats.p90 = getPercentile(sorted, 90); 286 stats.p99 = getPercentile(sorted, 99); 287 288 return stats; 289 } 290 291 private double getPercentile(List<Double> sorted, int percentile) { 292 if (sorted.isEmpty()) { 293 return 0; 294 } 295 int index = (sorted.size() * percentile) / 100; 296 if (index >= sorted.size()) { 297 index = sorted.size() - 1; 298 } 299 return sorted.get(index); 300 } 301 302 private double round(double value, int decimals) { 303 double scale = Math.pow(10, decimals); 304 return Math.round(value * scale) / scale; 305 } 306 307 private static class Statistics { 308 double mean = 0; 309 double min = 0; 310 double max = 0; 311 double stdDev = 0; 312 double p50 = 0; 313 double p75 = 0; 314 double p90 = 0; 315 double p99 = 0; 316 } 317}