All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cz.pumpitup.pn5.reporting.prometheus.PrometheusReporter Maven / Gradle / Ivy

There is a newer version: 0.8.38
Show newest version
package cz.pumpitup.pn5.reporting.prometheus;

import cz.pumpitup.pn5.config.ConfigHelper;
import cz.pumpitup.pn5.core.LogLevel;
import cz.pumpitup.pn5.reporting.spi.AbstractReporterSPI;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Gauge;
import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory;
import io.prometheus.client.exporter.PushGateway;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.util.Objects.requireNonNull;

public class PrometheusReporter extends AbstractReporterSPI {

    public static final String CONFIG_PREFIX = "pn5.reporting.prometheus.";
    public static final String CONFIG_KEY_PROMETHEUS_ENABLED = "enabled";
    public static final String CONFIG_KEY_PROMETHEUS_ENDPOINT = "endpoint";
    public static final String CONFIG_KEY_PROMETHEUS_USERNAME = "username";
    public static final String CONFIG_KEY_PROMETHEUS_PASSWORD = "password";
    public static final String CONFIG_KEY_PROMETHEUS_JOB_NAME = "job";
    public static final String CONFIG_KEY_CUSTOM_LABELS = "labels";

    public static final String KEY_SUITE_NAME = "suite";
    public static final String CONFIG_KEY_PROMETHEUS_TRACK_TESTCASES = "testcases.track";
    public static final String SUITE_LABEL = "suite_name";
    public static final String SUITE_METRIC = "suite_execution_gauge";
    public static final String SUITE_FINISH_METRIC = "suite_execution_finish_time";
    public static final String SUITE_DURATION_METRIC = "suite_execution_duration_seconds";
    public static final String TEST_CASE_LABEL = "test_case_name";
    public static final String TEST_EXECUTION_TOTAL = "test_execution_total";
    public static final String TEST_EXECUTION_FINISH_TIME = "test_execution_finish_time";
    public static final String TEST_EXECUTION_DURATION_SECONDS = "test_execution_duration_seconds";

    ExtensionContext extensionContext;
    PushGateway pushGateway = null;
    private CollectorRegistry registry = null;
    Gauge testExecutionTotalGauge = null;
    Gauge testExecutionDurationGauge = null;
    Gauge testExecutionFinishGauge = null;
    Gauge classExecutionGauge = null;
    Gauge classExecutionFinishGauge = null;
    Gauge classExecutionDurationGauge = null;
    private String jobName;
    private boolean trackTestcases;

    String[] classLabelKeys;
    String[] testLabelKeys;
    String[] classLabelValues;
    String[] testLabelValues;

    Map> testCaseStarted = new HashMap<>();
    Map testCaseCompleted = new HashMap<>();
    String displayId;
    String testSuiteName;

    String testCaseName;

    int total;
    int success;
    long start;

    long testStart;

    private final Lock lock = new ReentrantLock();

    private static final Map reporters = new HashMap<>();

    public static PrometheusReporter resolve(ExtensionContext extensionContext) {
        final Optional> testClass = extensionContext.getTestClass();
        if (testClass.isEmpty()) {
            return null;
        }
        String key = testClass.get().getName();
        if (!reporters.containsKey(key)) {
            final PrometheusReporter reporter = new PrometheusReporter().init(extensionContext);
            reporters.put(key, reporter);
        }
        return reporters.get(key);
    }

    protected PrometheusReporter init(ExtensionContext extensionContext) {
        super.init(extensionContext, CONFIG_PREFIX);

        this.extensionContext = extensionContext;

        if (!this.config.getOrDefault(CONFIG_KEY_PROMETHEUS_ENABLED, false)) {
            return null;
        }

        if (!config.containsKey(CONFIG_KEY_PROMETHEUS_ENDPOINT)) {
            logger.log(LogLevel.DEBUG, "PrometheusReporter is configured, but pn5.reporting.prometheus.endpoint is not. Will not push any metrics.");
        } else {
            String endpoint =
                    requireNonNull(config.get(CONFIG_KEY_PROMETHEUS_ENDPOINT),
                            "Missing endpoint in configuration under parameter \"pn5.reporting.prometheus.endpoint\". Please, check your configuration and try again.");
            try {
                this.pushGateway = new PushGateway(new URL(endpoint));
            } catch (MalformedURLException e) {
                throw new AssertionError("Endpoint must be a valid URL", e);
            }
        }

        this.jobName = config.get(CONFIG_KEY_PROMETHEUS_JOB_NAME);

        if (config.containsKey(CONFIG_KEY_PROMETHEUS_USERNAME)) {
            String username =
                    requireNonNull(config.get(CONFIG_KEY_PROMETHEUS_USERNAME),
                            "Missing username in configuration under parameter \"pn5.reporting.prometheus.username\". Please, check your configuration and try again.");
            String password =
                    requireNonNull(config.get(CONFIG_KEY_PROMETHEUS_PASSWORD),
                            "Missing password in configuration under parameter \"pn5.reporting.prometheus.password\". Please, check your configuration and try again.");

            this.pushGateway.setConnectionFactory(new BasicAuthHttpConnectionFactory(username, password));
        }

        this.trackTestcases = config.getBoolean(CONFIG_KEY_PROMETHEUS_TRACK_TESTCASES);

        this.registry = new CollectorRegistry();

        ArrayList classLabelKeys = new ArrayList<>();
        ArrayList classLabelValues = new ArrayList<>();

        classLabelKeys.add(SUITE_LABEL);
        classLabelValues.add("");

        ArrayList testLabelKeys = new ArrayList<>();
        ArrayList testLabelValues = new ArrayList<>();

        testLabelKeys.add(SUITE_LABEL);
        testLabelValues.add("");

        testLabelKeys.add(TEST_CASE_LABEL);
        testLabelValues.add("");

        if (config.containsKey(CONFIG_KEY_CUSTOM_LABELS)) {
            final StringTokenizer tokenizer = new StringTokenizer(config.get(CONFIG_KEY_CUSTOM_LABELS), ",");
            while (tokenizer.hasMoreTokens()) {
                final String token = tokenizer.nextToken();
                final String[] keyAndValue = token.split("=");

                if (keyAndValue.length != 2) {
                    logger.log(LogLevel.WARN, "Invalid custom label: {}, will ignore it. Make sure you use format 'key1=value1,key2=value2'", token);
                    continue;
                }

                String key = keyAndValue[0].trim();
                String value = keyAndValue[1].trim();

                if (key.isEmpty() || value.isEmpty()) {
                    logger.log(LogLevel.WARN, "Invalid custom label: {}, will ignore it. Make sure you use format 'key1=value1,key2=value2'", token);
                    continue;
                }

                logger.log(LogLevel.DEBUG, "Adding custom label: {}", token);

                classLabelKeys.add(key);
                classLabelValues.add(value);

                testLabelKeys.add(key);
                testLabelValues.add(value);
            }
        }

        this.testLabelKeys = testLabelKeys.toArray(new String[0]);
        this.testLabelValues = testLabelValues.toArray(new String[0]);

        this.classLabelKeys = classLabelKeys.toArray(new String[0]);
        this.classLabelValues = classLabelValues.toArray(new String[0]);

        this.classExecutionGauge = Gauge.build()
                .name(SUITE_METRIC)
                .help("Test suite execution status")
                .labelNames(this.classLabelKeys)
                .register(registry);

        this.classExecutionFinishGauge = Gauge.build()
                .name(SUITE_FINISH_METRIC)
                .help("Timestamp of finish of last execution of test suite")
                .labelNames(this.classLabelKeys)
                .register(registry);

        this.classExecutionDurationGauge = Gauge.build()
                .name(SUITE_DURATION_METRIC)
                .help("Duration in seconds of last execution of test suite")
                .labelNames(this.classLabelKeys)
                .register(registry);

        this.displayId = extensionContext.getDisplayName();

        return this;
    }

    @Override
    public void startTestcase(String testCaseName) {
        lock.lock();
        this.total++;
        if (this.trackTestcases) {

            try {
                if (this.testExecutionTotalGauge == null
                        && this.testExecutionFinishGauge == null
                        && this.testExecutionDurationGauge == null) {

                    this.testExecutionTotalGauge = Gauge.build()
                            .name(TEST_EXECUTION_TOTAL)
                            .help("Test case execution status")
                            .labelNames("suite_name", "test_name", "result")
                            .register(registry);

                    this.testExecutionFinishGauge = Gauge.build()
                            .name(TEST_EXECUTION_FINISH_TIME)
                            .help("Timestamp of finish of last execution of test case")
                            .labelNames("suite_name", "test_name")
                            .register(registry);

                    this.testExecutionDurationGauge = Gauge.build()
                            .name(TEST_EXECUTION_DURATION_SECONDS)
                            .help("Duration in seconds of last execution of test case")
                            .labelNames("suite_name", "test_name")
                            .register(registry);
                }

                Map mapTestCase = new HashMap<>();
                mapTestCase.put("started", Instant.now().getEpochSecond());
                mapTestCase.put("order", (long) total);

                this.testCaseStarted.put(testCaseName, mapTestCase);

                this.testCaseCompleted.put(testCaseName, false);
            } finally {
                lock.unlock();
            }
        }
    }

    @Override
    public void completeTestcase(String testCaseName) {
        lock.lock();
        try {
            // This method in only called by JUnitReportingExtension if the test case passed
            // In case the test fails, the 'success' counter just stays the same
            success++;

            if (this.trackTestcases) {
                this.testLabelValues = new String[3];
                this.testLabelValues[0] = testSuiteName;
                this.testLabelValues[1] = testCaseName;
                this.testLabelValues[2] = "passed";
                this.testExecutionTotalGauge.labels(this.testLabelValues).set(this.testCaseStarted.get(testCaseName).get("order"));
                this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus (complete)", TEST_EXECUTION_TOTAL, Arrays.toString(this.testLabelValues));
                pushSafely(this.jobName);

                pushGenericTestCaseMetrics(testCaseName);

                this.testCaseCompleted.put(testCaseName, true);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void completeTestcaseFailed(String testCaseName) {
        lock.lock();
        try {
            if (this.trackTestcases) {
                this.testLabelValues = new String[3];
                this.testLabelValues[0] = testSuiteName;
                this.testLabelValues[1] = testCaseName;
                this.testLabelValues[2] = "failed";
                this.testExecutionTotalGauge.labels(this.testLabelValues).set(this.testCaseStarted.get(testCaseName).get("order"));
                this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus (complete)", TEST_EXECUTION_TOTAL, Arrays.toString(this.testLabelValues));
                pushSafely(this.jobName);

                pushGenericTestCaseMetrics(testCaseName);

                this.testCaseCompleted.put(testCaseName, true);
            }
        } finally {
            lock.unlock();
        }
    }

    private void pushGenericTestCaseMetrics(String testCaseName) {
        final Instant end = Instant.now();

        this.testLabelValues = new String[2];
        this.testLabelValues[0] = testSuiteName;
        this.testLabelValues[1] = testCaseName;
        this.testExecutionFinishGauge.labels(this.testLabelValues).set(end.getEpochSecond());
        this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus (complete)", TEST_EXECUTION_FINISH_TIME, Arrays.toString(this.testLabelValues));
        pushSafely(this.jobName);

        this.testLabelValues = new String[2];
        this.testLabelValues[0] = testSuiteName;
        this.testLabelValues[1] = testCaseName;
        long duration = end.minus(this.testCaseStarted.get(testCaseName).get("started"), ChronoUnit.SECONDS).getEpochSecond();
        this.testExecutionDurationGauge.labels(this.testLabelValues).set(duration);
        this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus (complete)", TEST_EXECUTION_DURATION_SECONDS, Arrays.toString(this.testLabelValues));
        pushSafely(this.jobName);
    }

    @Override
    public void startTestSuite() {
        String testSuiteName = config.get(KEY_SUITE_NAME);
        if(testSuiteName == null) {
            testSuiteName = extensionContext.getDisplayName();
        }

        this.testSuiteName = testSuiteName;
        if(this.jobName == null) {
            this.jobName = testSuiteName;
        }
        success = 0;
        total = 0;
        start = Instant.now().getEpochSecond();
    }

    @Override
    public void completeTestSuite() {
        this.classLabelValues[0] = testSuiteName;

        final Instant end = Instant.now();
        classExecutionFinishGauge.labels(this.classLabelValues).set(end.getEpochSecond());

        final long duration = end.minus(start, ChronoUnit.SECONDS).getEpochSecond();
        classExecutionDurationGauge.labels(this.classLabelValues).set(duration);

        double successRatio = 0;
        if (total != 0) {
            successRatio = Math.round(success / (double) total * 100.0) / 100.0;
        }

        classExecutionGauge.labels(this.classLabelValues).set(successRatio);
        this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus", SUITE_FINISH_METRIC, Arrays.toString(this.classLabelValues));
        this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus", SUITE_DURATION_METRIC, Arrays.toString(this.classLabelValues));
        this.logger.log(LogLevel.INFO, "Pushing metric {} with labels {} to Prometheus", SUITE_METRIC, Arrays.toString(this.classLabelValues));
        pushSafely(this.jobName);
    }

    private void pushSafely(String jobName) {
        if (pushGateway != null) {
            try {
                this.pushGateway.pushAdd(this.registry, jobName);
            } catch (IOException e) {
                logger.log(LogLevel.WARN, "Exception pushing metrics to gateway: {}", ExceptionUtils.getStackTrace(e));
            }
        }
    }

    @Override
    public String getTestcaseKey() {
        return null;
    }

    @Override
    public boolean markTestAsStarted() {
        return true;
    }

    @Override
    public void markTestAsPassed() {

    }

    @Override
    public void markTestAsFailed() {

    }

    @Override
    public void markStepAsStarted(int step) {

    }

    @Override
    public void markStepAsSkipped(int step) {

    }

    @Override
    public void markStepAsPassed(int step) {

    }

    @Override
    public void markStepAsFailed(int step) {

    }

    @Override
    public void addComment(String comment, Integer step) {

    }

    @Override
    public void addThrowable(Throwable throwable, Integer step) {

    }

    @Override
    public void attach(String name, String fileExtension, String type, InputStream stream, Integer step) {

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy