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}