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 de.cuioss.benchmarking.common.config.BenchmarkConfiguration;
019import de.cuioss.tools.logging.CuiLogger;
020import org.openjdk.jmh.results.RunResult;
021
022import java.io.IOException;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.time.Duration;
026import java.time.Instant;
027import java.util.Collection;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.ConcurrentHashMap;
031
032import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.ERROR.FAILED_COLLECT_PROMETHEUS_BENCHMARK;
033import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.ERROR.PROMETHEUS_CONNECTIVITY_FAILED;
034import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.INFO.*;
035import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.WARN.*;
036
037/**
038 * Centralized manager for Prometheus metrics collection during benchmark execution.
039 * This class provides a unified approach to collecting real-time metrics from Prometheus
040 * for both JMH and WRK benchmarks.
041 *
042 * <p>The manager tracks benchmark execution timestamps and collects metrics
043 * at the end of benchmark runs. This ensures consistent metrics collection
044 * across different benchmark types.</p>
045 *
046 * <p>Features:</p>
047 * <ul>
048 *   <li>Thread-safe timestamp tracking for concurrent benchmarks</li>
049 *   <li>Automatic Prometheus client configuration from system properties</li>
050 *   <li>Unified metrics collection for different benchmark result types</li>
051 *   <li>Integration with MetricsOrchestrator for actual metrics retrieval</li>
052 * </ul>
053 */
054public class PrometheusMetricsManager {
055
056    private static final CuiLogger LOGGER = new CuiLogger(PrometheusMetricsManager.class);
057
058    public static final String PROMETHEUS_DIR_NAME = "prometheus";
059    public static final String PROMETHEUS_URL_PROPERTY = "prometheus.url";
060    public static final String PROMETHEUS_URL_DEFAULT = "http://localhost:9090";
061    public static final String METRICS_FILE_SUFFIX = "-metrics.json";
062
063    private final Map<String, BenchmarkTimestamps> timestampTracker = new ConcurrentHashMap<>();
064    private final String prometheusUrl;
065    private final boolean metricsEnabled;
066
067    /**
068     * Data class to hold benchmark execution timestamps.
069     */
070    private record BenchmarkTimestamps(Instant startTime, Instant endTime) {
071        boolean isValid() {
072            return startTime != null && endTime != null;
073        }
074    }
075
076    /**
077     * Creates a new PrometheusMetricsManager.
078     * Prometheus URL is read from system property or defaults to localhost:9090.
079     */
080    public PrometheusMetricsManager() {
081        this.prometheusUrl = System.getProperty(PROMETHEUS_URL_PROPERTY, PROMETHEUS_URL_DEFAULT);
082        this.metricsEnabled = isPrometheusAvailable();
083
084        if (metricsEnabled) {
085            LOGGER.info(PROMETHEUS_ENABLED, prometheusUrl);
086        } else {
087            LOGGER.info(PROMETHEUS_DISABLED, prometheusUrl);
088        }
089    }
090
091    /**
092     * Records the start time for a benchmark execution.
093     * This should be called just before benchmark execution begins.
094     *
095     * @param benchmarkName the name of the benchmark
096     */
097    public void recordBenchmarkStart(String benchmarkName) {
098        Instant startTime = Instant.now();
099        timestampTracker.compute(benchmarkName, (key, existing) -> {
100            if (existing != null) {
101                LOGGER.warn(OVERWRITING_START_TIME, benchmarkName);
102            }
103            return new BenchmarkTimestamps(startTime, null);
104        });
105        LOGGER.debug("Benchmark '%s' started at: %s", benchmarkName, startTime);
106    }
107
108    /**
109     * Records both start and end timestamps for a benchmark.
110     * Used when timestamps are provided externally (e.g., from WRK output).
111     *
112     * @param benchmarkName the name of the benchmark
113     * @param startTime the benchmark start time
114     * @param endTime the benchmark end time
115     */
116    public void recordBenchmarkTimestamps(String benchmarkName, Instant startTime, Instant endTime) {
117        if (startTime == null || endTime == null) {
118            LOGGER.warn(INVALID_TIMESTAMPS, benchmarkName, startTime, endTime);
119            return;
120        }
121
122        timestampTracker.put(benchmarkName, new BenchmarkTimestamps(startTime, endTime));
123        LOGGER.debug("Recorded timestamps for benchmark '%s': %s to %s",
124                benchmarkName, startTime, endTime);
125    }
126
127    /**
128     * Collects Prometheus metrics for completed benchmarks.
129     * This should be called after all benchmarks have completed execution.
130     *
131     * @param results the benchmark results
132     * @param config the benchmark configuration
133     */
134    public void collectMetricsForResults(Collection<RunResult> results, BenchmarkConfiguration config) {
135        if (!metricsEnabled) {
136            LOGGER.debug("Skipping Prometheus metrics collection - not enabled");
137            return;
138        }
139
140        if (!config.hasIntegrationConfig()) {
141            LOGGER.debug("Skipping Prometheus metrics collection - no integration config");
142            return;
143        }
144
145        String outputDirectory = config.resultsDirectory();
146
147        try {
148            Path prometheusDir = Path.of(outputDirectory, PROMETHEUS_DIR_NAME);
149            Files.createDirectories(prometheusDir);
150
151            LOGGER.info(COLLECTING_PROMETHEUS_METRICS, prometheusUrl);
152
153            MetricsOrchestrator orchestrator = new MetricsOrchestrator(
154                    new PrometheusClient(prometheusUrl)
155            );
156
157            for (RunResult result : results) {
158                String benchmarkName = extractBenchmarkName(result);
159                collectMetricsForBenchmark(benchmarkName, orchestrator, prometheusDir);
160            }
161
162        } catch (IOException e) {
163            LOGGER.warn(FAILED_COLLECT_PROMETHEUS, e.getMessage());
164        }
165    }
166
167    /**
168     * Collects Prometheus metrics for WRK benchmark results.
169     * This method handles the specific case of WRK benchmarks where
170     * timestamps are extracted from the output files.
171     *
172     * @param benchmarkName the name of the benchmark
173     * @param startTime the benchmark start time
174     * @param endTime the benchmark end time
175     * @param outputDirectory the directory to save metrics
176     */
177    public void collectMetricsForWrkBenchmark(String benchmarkName, Instant startTime,
178            Instant endTime, String outputDirectory) {
179        if (!metricsEnabled) {
180            LOGGER.warn(SKIPPING_PROMETHEUS_COLLECTION, prometheusUrl);
181            LOGGER.debug("To enable metrics collection, ensure Prometheus is running and accessible at the configured URL");
182            return;
183        }
184
185        if (startTime == null || endTime == null) {
186            LOGGER.warn(MISSING_TIMESTAMPS_FOR_COLLECTION, benchmarkName);
187            return;
188        }
189
190        try {
191            Path prometheusDir = Path.of(outputDirectory, PROMETHEUS_DIR_NAME);
192            Files.createDirectories(prometheusDir);
193
194            LOGGER.info(COLLECTING_WRK_PROMETHEUS_METRICS, benchmarkName);
195
196            MetricsOrchestrator orchestrator = new MetricsOrchestrator(
197                    new PrometheusClient(prometheusUrl)
198            );
199
200            orchestrator.collectBenchmarkMetrics(
201                    benchmarkName,
202                    startTime,
203                    endTime,
204                    prometheusDir
205            );
206
207            LOGGER.info(PROMETHEUS_METRICS_SAVED, prometheusDir, benchmarkName, METRICS_FILE_SUFFIX);
208
209        } catch (IOException e) {
210            LOGGER.error(e, FAILED_COLLECT_PROMETHEUS_BENCHMARK, benchmarkName, prometheusUrl);
211            LOGGER.debug("Attempted to query metrics for time range: %s to %s", startTime, endTime);
212        }
213    }
214
215    private void collectMetricsForBenchmark(String benchmarkName, MetricsOrchestrator orchestrator,
216            Path prometheusDir) {
217        LOGGER.debug("Collecting metrics for benchmark: '%s', available keys: %s",
218                benchmarkName, timestampTracker.keySet());
219
220        // Get the specific timestamps for this benchmark
221        BenchmarkTimestamps timestamps = timestampTracker.get(benchmarkName);
222
223        if (timestamps == null || !timestamps.isValid()) {
224            LOGGER.warn(NO_VALID_TIMESTAMPS, benchmarkName, timestampTracker.keySet());
225            return;
226        }
227
228        LOGGER.info(USING_SESSION_TIMESTAMPS, benchmarkName, timestamps.startTime(), timestamps.endTime());
229
230        try {
231            LOGGER.info(COLLECTING_BENCHMARK_METRICS, benchmarkName);
232
233            orchestrator.collectBenchmarkMetrics(
234                    benchmarkName,
235                    timestamps.startTime(),
236                    timestamps.endTime(),
237                    prometheusDir
238            );
239
240            LOGGER.info(PROMETHEUS_METRICS_SAVED, prometheusDir, benchmarkName, METRICS_FILE_SUFFIX);
241
242        } catch (IOException e) {
243            LOGGER.warn(FAILED_COLLECT_BENCHMARK_METRICS, benchmarkName, e.getMessage());
244        }
245    }
246
247    private String extractBenchmarkName(RunResult result) {
248        String label = result.getPrimaryResult().getLabel();
249        int lastDot = label.lastIndexOf('.');
250        if (lastDot >= 0 && lastDot < label.length() - 1) {
251            return label.substring(lastDot + 1);
252        }
253        return label;
254    }
255
256    private boolean isPrometheusAvailable() {
257        try {
258            LOGGER.debug("Checking Prometheus availability at URL: %s", prometheusUrl);
259            PrometheusClient client = new PrometheusClient(prometheusUrl);
260            client.queryRange(List.of("up"), Instant.now().minusSeconds(60), Instant.now(), Duration.ofSeconds(10));
261            LOGGER.debug("Prometheus is available and responding at: %s", prometheusUrl);
262            return true;
263        } catch (PrometheusClient.PrometheusException e) {
264            LOGGER.error(e, PROMETHEUS_CONNECTIVITY_FAILED, prometheusUrl, e.getMessage(), e.getClass().getSimpleName());
265            LOGGER.info(PROMETHEUS_CONNECTIVITY_ADVICE, prometheusUrl);
266            return false;
267        }
268    }
269
270    /**
271     * Clears all tracked timestamps.
272     * Useful for testing or when reusing the manager for multiple benchmark runs.
273     */
274    public void clear() {
275        timestampTracker.clear();
276    }
277
278}