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 com.google.gson.*; 019import com.google.gson.reflect.TypeToken; 020import de.cuioss.tools.logging.CuiLogger; 021 022import java.io.FileWriter; 023import java.io.IOException; 024import java.lang.reflect.Type; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.text.DecimalFormat; 028import java.text.DecimalFormatSymbols; 029import java.time.Instant; 030import java.util.LinkedHashMap; 031import java.util.Locale; 032import java.util.Map; 033import java.util.regex.Matcher; 034import java.util.regex.Pattern; 035 036import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.WARN.*; 037 038/** 039 * Exports metrics data to JSON files in a target directory. 040 * Handles JSON serialization, file writing, and metrics aggregation. 041 * 042 */ 043public class MetricsJsonExporter { 044 045 private static final CuiLogger LOGGER = new CuiLogger(MetricsJsonExporter.class); 046 047 private static final Gson GSON = new GsonBuilder() 048 .setPrettyPrinting() 049 .serializeNulls() 050 .registerTypeAdapter(Instant.class, (JsonSerializer<Instant>) 051 (src, typeOfSrc, context) -> new JsonPrimitive(src.toString())) 052 .registerTypeAdapter(Number.class, (JsonSerializer<Number>) (src, typeOfSrc, context) -> { 053 if (src instanceof Double || src instanceof Float) { 054 double value = src.doubleValue(); 055 if (value == Math.floor(value) && !Double.isInfinite(value)) { 056 return new JsonPrimitive(src.longValue()); 057 } 058 return new JsonPrimitive(value); 059 } 060 return new JsonPrimitive(src); 061 }) 062 .create(); 063 public static final String BEARER_TOKEN_RESULT = "getBearerTokenResult"; 064 065 private final Path targetDirectory; 066 067 /** 068 * Creates a new metrics JSON exporter. 069 * 070 * @param targetDirectory Directory where JSON files will be written 071 */ 072 public MetricsJsonExporter(Path targetDirectory) { 073 this.targetDirectory = targetDirectory; 074 try { 075 Files.createDirectories(targetDirectory); 076 LOGGER.debug("MetricsJsonExporter initialized with target directory: %s (exists: %s)", 077 targetDirectory.toAbsolutePath(), Files.exists(targetDirectory)); 078 } catch (IOException e) { 079 LOGGER.warn(e, FAILED_CREATE_TARGET_DIR, targetDirectory); 080 } 081 } 082 083 /** 084 * Exports metrics data to a JSON file. 085 * 086 * @param fileName The name of the JSON file 087 * @param metricsData The metrics data to export 088 * @throws IOException if writing fails 089 */ 090 public void exportToFile(String fileName, Map<String, Object> metricsData) throws IOException { 091 Path outputFile = targetDirectory.resolve(fileName); 092 093 try (FileWriter writer = new FileWriter(outputFile.toFile())) { 094 GSON.toJson(metricsData, writer); 095 writer.flush(); 096 LOGGER.debug("Exported metrics to: %s", outputFile.toAbsolutePath()); 097 } 098 } 099 100 /** 101 * Exports JWT validation metrics for a specific benchmark method. 102 * 103 * @param benchmarkMethodName The name of the benchmark method 104 * @param timestamp The timestamp when the benchmark was executed 105 * @param allMetrics All metrics data 106 * @throws IOException if export fails 107 */ 108 public void exportJwtValidationMetrics(String benchmarkMethodName, Instant timestamp, 109 Map<String, Double> allMetrics) throws IOException { 110 LOGGER.debug("Exporting JWT validation metrics for: %s", benchmarkMethodName); 111 112 if (isJwtValidationBenchmark(benchmarkMethodName)) { 113 Map<String, Object> timedMetrics = extractTimedMetrics(allMetrics); 114 Map<String, Object> securityEventMetrics = extractSecurityEventMetrics(allMetrics); 115 116 Map<String, Object> benchmarkData = new LinkedHashMap<>(); 117 benchmarkData.put("timestamp", timestamp.toString()); 118 benchmarkData.put("bearer_token_producer_metrics", timedMetrics); 119 benchmarkData.put("security_event_counter_metrics", securityEventMetrics); 120 121 String simpleBenchmarkName = extractSimpleBenchmarkName(benchmarkMethodName); 122 updateAggregatedMetrics("integration-metrics.json", simpleBenchmarkName, benchmarkData); 123 } else { 124 LOGGER.debug("Benchmark %s is not JWT validation, raw metrics were saved", benchmarkMethodName); 125 } 126 } 127 128 /** 129 * Exports resource metrics (CPU, memory) for system monitoring. 130 * 131 * @param timestamp The timestamp when metrics were collected 132 * @param allMetrics All metrics data 133 * @throws IOException if export fails 134 */ 135 public void exportResourceMetrics(Instant timestamp, Map<String, Double> allMetrics) throws IOException { 136 LOGGER.debug("Exporting resource metrics"); 137 138 Map<String, Object> resourceData = new LinkedHashMap<>(); 139 resourceData.put("timestamp", timestamp.toString()); 140 resourceData.put("cpu_metrics", extractCpuMetrics(allMetrics)); 141 resourceData.put("memory_metrics", extractMemoryMetrics(allMetrics)); 142 143 exportToFile("resource-metrics.json", resourceData); 144 } 145 146 /** 147 * Updates an aggregated metrics file with new benchmark data. 148 * For quarkus-metrics.json files, applies special transformation to create the new structure. 149 * 150 * @param fileName The name of the aggregated metrics file 151 * @param benchmarkName The name of the benchmark 152 * @param benchmarkData The benchmark data to add/update 153 * @throws IOException if writing fails 154 */ 155 public void updateAggregatedMetrics(String fileName, String benchmarkName, 156 Map<String, Object> benchmarkData) throws IOException { 157 Map<String, Object> allMetrics = readExistingMetrics(fileName); 158 159 if ("quarkus-metrics.json".equals(fileName)) { 160 // Apply special transformation for Quarkus runtime metrics 161 String transformedKey = "quarkus-runtime-metrics"; 162 Map<String, Object> transformedData = transformToQuarkusRuntimeMetrics(benchmarkData); 163 allMetrics.put(transformedKey, transformedData); 164 } else { 165 allMetrics.put(benchmarkName, benchmarkData); 166 } 167 168 exportToFile(fileName, allMetrics); 169 LOGGER.debug("Updated %s with %s benchmarks", fileName, allMetrics.size()); 170 } 171 172 /** 173 * Transforms benchmark data into the new Quarkus runtime metrics structure. 174 * Removes the "benchmark" field and ensures proper structure. 175 */ 176 private Map<String, Object> transformToQuarkusRuntimeMetrics(Map<String, Object> originalData) { 177 Map<String, Object> transformed = new LinkedHashMap<>(originalData); 178 179 // Remove the "benchmark" field if present 180 transformed.remove("benchmark"); 181 182 return transformed; 183 } 184 185 /** 186 * Reads existing metrics from a JSON file. 187 * 188 * @param fileName The name of the JSON file 189 * @return The parsed metrics map, or empty map if file doesn't exist or is empty 190 */ 191 public Map<String, Object> readExistingMetrics(String fileName) { 192 Map<String, Object> existingMetrics = new LinkedHashMap<>(); 193 Path filePath = targetDirectory.resolve(fileName); 194 195 if (Files.exists(filePath)) { 196 try { 197 String content = Files.readString(filePath); 198 if (!content.trim().isEmpty()) { 199 Type mapType = new TypeToken<Map<String, Object>>(){ 200 }.getType(); 201 Map<String, Object> parsed = GSON.fromJson(content, mapType); 202 if (parsed != null) { 203 existingMetrics = parsed; 204 } 205 } 206 } catch (IOException | JsonSyntaxException e) { 207 LOGGER.warn(FAILED_READ_METRICS_FILE, fileName, e.getMessage()); 208 try { 209 Files.deleteIfExists(filePath); 210 } catch (IOException deleteException) { 211 LOGGER.warn(deleteException, FAILED_DELETE_CORRUPTED_FILE); 212 } 213 } 214 } 215 216 return existingMetrics; 217 } 218 219 private boolean isJwtValidationBenchmark(String benchmarkMethodName) { 220 return benchmarkMethodName.contains("JwtValidationBenchmark") || 221 "JwtValidation".equals(benchmarkMethodName) || 222 benchmarkMethodName.startsWith("validateJwt") || 223 "validateJwtToken".equals(benchmarkMethodName) || 224 benchmarkMethodName.contains("validateAccessToken") || 225 benchmarkMethodName.contains("validateIdToken"); 226 } 227 228 private Map<String, Object> extractTimedMetrics(Map<String, Double> allMetrics) { 229 Map<String, Object> timedMetrics = new LinkedHashMap<>(); 230 231 Double count = null; 232 Double sum = null; 233 Double max = null; 234 235 for (Map.Entry<String, Double> entry : allMetrics.entrySet()) { 236 String metricName = entry.getKey(); 237 Double value = entry.getValue(); 238 239 if (metricName.contains("bearer_token_validation_seconds")) { 240 if (metricName.contains("_count") && metricName.contains(BEARER_TOKEN_RESULT)) { 241 count = value; 242 } else if (metricName.contains("_sum") && metricName.contains(BEARER_TOKEN_RESULT)) { 243 sum = value; 244 } else if (metricName.contains("_max") && metricName.contains(BEARER_TOKEN_RESULT)) { 245 max = value; 246 } 247 } 248 } 249 250 if (count != null && sum != null && max != null && count > 0) { 251 double avgMicros = (sum / count) * 1_000_000; 252 253 Map<String, Object> validationMetric = new LinkedHashMap<>(); 254 validationMetric.put("sample_count", formatNumber(count.longValue())); 255 validationMetric.put("p50_us", formatNumber(avgMicros)); 256 validationMetric.put("p95_us", formatNumber(Math.min(avgMicros * 2, max * 1_000_000 * 0.8))); 257 validationMetric.put("p99_us", formatNumber(max * 1_000_000 * 0.9)); 258 timedMetrics.put("validation", validationMetric); 259 } else { 260 Map<String, Object> emptyMetric = new LinkedHashMap<>(); 261 emptyMetric.put("sample_count", formatNumber(0)); 262 emptyMetric.put("p50_us", formatNumber(0)); 263 emptyMetric.put("p95_us", formatNumber(0)); 264 emptyMetric.put("p99_us", formatNumber(0)); 265 timedMetrics.put("validation", emptyMetric); 266 } 267 268 return timedMetrics; 269 } 270 271 private Map<String, Object> extractSecurityEventMetrics(Map<String, Double> allMetrics) { 272 Map<String, Object> securityMetrics = new LinkedHashMap<>(); 273 Map<String, Map<String, Object>> errorsByCategory = new LinkedHashMap<>(); 274 Map<String, Object> successByType = new LinkedHashMap<>(); 275 long totalErrors = 0; 276 long totalSuccess = 0; 277 278 for (Map.Entry<String, Double> entry : allMetrics.entrySet()) { 279 String metricName = entry.getKey(); 280 Double value = entry.getValue(); 281 282 if (metricName.startsWith("sheriff_oauth_validation_errors_total")) { 283 String category = extractTag(metricName, "category"); 284 String eventType = extractTag(metricName, "event_type"); 285 286 if (category != null && eventType != null && value != null && value > 0) { 287 Map<String, Object> categoryData = errorsByCategory.computeIfAbsent(category, k -> new LinkedHashMap<>()); 288 categoryData.put(eventType, formatNumber(value.longValue())); 289 totalErrors += value.longValue(); 290 } 291 } else if (metricName.startsWith("sheriff_oauth_validation_success")) { 292 String eventType = extractTag(metricName, "event_type"); 293 String result = extractTag(metricName, "result"); 294 295 if (eventType != null && "success".equals(result) && value != null && value > 0) { 296 successByType.put(eventType, formatNumber(value.longValue())); 297 totalSuccess += value.longValue(); 298 } 299 } 300 } 301 302 securityMetrics.put("total_errors", formatNumber(totalErrors)); 303 securityMetrics.put("total_success", formatNumber(totalSuccess)); 304 securityMetrics.put("errors_by_category", errorsByCategory); 305 securityMetrics.put("success_by_type", successByType); 306 307 return securityMetrics; 308 } 309 310 private Map<String, Object> extractCpuMetrics(Map<String, Double> allMetrics) { 311 Map<String, Object> cpuMetrics = new LinkedHashMap<>(); 312 313 for (Map.Entry<String, Double> entry : allMetrics.entrySet()) { 314 String metricName = entry.getKey(); 315 Double value = entry.getValue(); 316 317 if (metricName.startsWith("system_cpu_usage")) { 318 cpuMetrics.put("system_cpu_usage", formatPercentage(value)); 319 } else if (metricName.startsWith("process_cpu_usage")) { 320 cpuMetrics.put("process_cpu_usage", formatPercentage(value)); 321 } else if (metricName.startsWith("system_cpu_count")) { 322 cpuMetrics.put("cpu_count", value.intValue()); 323 } 324 } 325 326 return cpuMetrics; 327 } 328 329 private Map<String, Object> extractMemoryMetrics(Map<String, Double> allMetrics) { 330 Map<String, Object> memoryMetrics = new LinkedHashMap<>(); 331 332 for (Map.Entry<String, Double> entry : allMetrics.entrySet()) { 333 String metricName = entry.getKey(); 334 Double value = entry.getValue(); 335 336 if (metricName.startsWith("jvm_memory_used_bytes")) { 337 String area = extractTag(metricName, "area"); 338 if ("heap".equals(area)) { 339 memoryMetrics.put("heap_used_bytes", value.longValue()); 340 } else if ("nonheap".equals(area)) { 341 memoryMetrics.put("nonheap_used_bytes", value.longValue()); 342 } 343 } 344 } 345 346 return memoryMetrics; 347 } 348 349 private String extractTag(String metricName, String tagName) { 350 String pattern = tagName + "=\"([^\"]+)\""; 351 Pattern regex = Pattern.compile(pattern); 352 Matcher matcher = regex.matcher(metricName); 353 return matcher.find() ? matcher.group(1) : null; 354 } 355 356 private Object formatNumber(double value) { 357 if (value < 10) { 358 DecimalFormat df = new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.US)); 359 return Double.parseDouble(df.format(value)); 360 } else { 361 return Math.round(value); 362 } 363 } 364 365 private Object formatPercentage(double value) { 366 double percentage = value * 100.0; 367 if (percentage < 10) { 368 return Math.round(percentage * 10.0) / 10.0; 369 } else { 370 return Math.round(percentage); 371 } 372 } 373 374 private String extractSimpleBenchmarkName(String fullBenchmarkName) { 375 if (fullBenchmarkName.contains(".")) { 376 return fullBenchmarkName.substring(fullBenchmarkName.lastIndexOf('.') + 1); 377 } 378 return fullBenchmarkName; 379 } 380}