diff --git a/.gitignore b/.gitignore index 331dc36fa..be0d22fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ work .classpath .settings/ build/ -*.DS_Store \ No newline at end of file +*.DS_Store +.vscode/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e0f15db2e..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file diff --git a/docs/metrics/index.md b/docs/metrics/index.md index 02a6244bc..0cb46e13b 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -52,6 +52,9 @@ will just return the last build. You can enable per build metrics in the configu | default_jenkins_builds_duration_milliseconds_summary | Summary of Jenkins build times in milliseconds by Job | summary | | default_jenkins_builds_success_build_count | Successful build count | counter | | default_jenkins_builds_failed_build_count | Failed build count | counter | +| default_jenkins_builds_unstable_build_count | Unstable build count | counter | +| default_jenkins_builds_total_build_count | Total build count (excluding not_built statuses) | counter | +| default_jenkins_builds_aborted_build_count | Aborted build count | counter | | default_jenkins_builds_health_score | Health score of a job | gauge | | default_jenkins_builds__last_build_result_ordinal | Build status of a job (0=SUCCESS,1=UNSTABLE,2=FAILURE,3=NOT_BUILT,4=ABORTED) | gauge | | default_jenkins_builds__last_build_result | Build status of a job as a boolean value - 0 or 1. Where 0 is: SUCCESS,UNSTABLE and 1: all other States | gauge | diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java b/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java index 3b2180cc4..ca417e156 100644 --- a/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java +++ b/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java @@ -1,14 +1,16 @@ package org.jenkinsci.plugins.prometheus; import hudson.model.Job; -import hudson.model.Result; import hudson.model.Run; import io.prometheus.client.Collector; -import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.ArrayUtils; import org.jenkinsci.plugins.prometheus.collectors.CollectorFactory; import org.jenkinsci.plugins.prometheus.collectors.CollectorType; import org.jenkinsci.plugins.prometheus.collectors.MetricCollector; +import org.jenkinsci.plugins.prometheus.collectors.builds.BuildCompletionListener; +import org.jenkinsci.plugins.prometheus.collectors.builds.CounterManager; +import org.jenkinsci.plugins.prometheus.collectors.builds.JobLabel; +import org.jenkinsci.plugins.prometheus.collectors.builds.BuildCompletionListener.CloseableIterator; import org.jenkinsci.plugins.prometheus.config.PrometheusConfiguration; import org.jenkinsci.plugins.prometheus.util.Jobs; import org.jenkinsci.plugins.prometheus.util.Runs; @@ -18,7 +20,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; public class JobCollector extends Collector { @@ -29,6 +30,9 @@ public class JobCollector extends Collector { private MetricCollector, ? extends Collector> summary; private MetricCollector, ? extends Collector> jobSuccessCount; private MetricCollector, ? extends Collector> jobFailedCount; + private MetricCollector, ? extends Collector> jobAbortedCount; + private MetricCollector, ? extends Collector> jobUnstableCount; + private MetricCollector, ? extends Collector> jobTotalCount; private MetricCollector, ? extends Collector> jobHealthScoreGauge; private MetricCollector, ? extends Collector> nbBuildsGauge; private MetricCollector, ? extends Collector> buildDiscardGauge; @@ -71,8 +75,7 @@ public void initCollectors(String[] labelNameArray) { private final BuildMetrics lastBuildMetrics = new BuildMetrics("last"); private final BuildMetrics perBuildMetrics = new BuildMetrics(""); - public JobCollector() { - } + public JobCollector() {} @Override public List collect() { @@ -113,13 +116,40 @@ public List collect() { return samples; } - // Below three metrics use labelNameArray which might include the optional labels + // Below metrics use labelNameArray which might include the optional labels // of "parameters" or "status" summary = factory.createRunCollector(CollectorType.BUILD_DURATION_SUMMARY, labelNameArray, null); - - jobSuccessCount = factory.createRunCollector(CollectorType.BUILD_SUCCESSFUL_COUNTER, labelNameArray, null); - - jobFailedCount = factory.createRunCollector(CollectorType.BUILD_FAILED_COUNTER, labelNameArray, null); + BuildCompletionListener listener = BuildCompletionListener.getInstance(); + + // Counter manager acts as a DB to retrieve any counters that are already in memory instead of reinitializing + // them with each iteration of collect. + var manager = CounterManager.getManager(); + jobSuccessCount = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, labelBaseNameArray, null); + jobFailedCount = manager.getCounter(CollectorType.BUILD_FAILED_COUNTER, labelBaseNameArray, null); + jobTotalCount = manager.getCounter(CollectorType.BUILD_TOTAL_COUNTER, labelBaseNameArray, null); + jobAbortedCount = manager.getCounter(CollectorType.BUILD_ABORTED_COUNTER, labelBaseNameArray, null); + jobUnstableCount = manager.getCounter(CollectorType.BUILD_UNSTABLE_COUNTER, labelBaseNameArray, null); + + // This is a try with resources block it ensures close is called + // so if an exception occurs we don't reach deadlock. This is analogous to a using + // block where dispose is called after we leave the block. + // The closeable iterator synchronizes receiving jobs and reading the iterator + // so we don't modify the collection while iterating. + try (CloseableIterator> iterator = listener.iterator()) { + // Go through each run received since the last scrape. + while (iterator.hasNext()) { + Run run = iterator.next(); + Job job = run.getParent(); + + // Calculate the metrics. + String[] labelValues = JobLabel.getBaseLabelValues(job); + jobFailedCount.calculateMetric(run, labelValues); + jobSuccessCount.calculateMetric(run, labelValues); + jobTotalCount.calculateMetric(run, labelValues); + jobAbortedCount.calculateMetric(run, labelValues); + jobUnstableCount.calculateMetric(run,labelValues); + } + } // This metric uses "base" labels as it is just the health score reported // by the job object and the optional labels params and status don't make much @@ -165,6 +195,9 @@ public List collect() { addSamples(samples, summary.collect(), "Adding [{}] samples from summary ({})"); addSamples(samples, jobSuccessCount.collect(), "Adding [{}] samples from counter ({})"); addSamples(samples, jobFailedCount.collect(), "Adding [{}] samples from counter ({})"); + addSamples(samples, jobAbortedCount.collect(), "Adding [{}] samples from counter ({})"); + addSamples(samples, jobUnstableCount.collect(), "Adding [{}] samples from counter ({})"); + addSamples(samples, jobTotalCount.collect(), "Adding [{}] samples from counter ({})"); addSamples(samples, jobHealthScoreGauge.collect(), "Adding [{}] samples from gauge ({})"); addSamples(samples, nbBuildsGauge.collect(), "Adding [{}] samples from gauge ({})"); addSamples(samples, buildDiscardGauge.collect(), "Adding [{}] samples from gauge ({})"); @@ -201,17 +234,8 @@ private void addSamples(List allSamples, BuildMetrics build } protected void appendJobMetrics(Job job) { - boolean isAppendParamLabel = PrometheusConfiguration.get().isAppendParamLabel(); - boolean isAppendStatusLabel = PrometheusConfiguration.get().isAppendStatusLabel(); boolean isPerBuildMetrics = PrometheusConfiguration.get().isPerBuildMetrics(); - String[] buildParameterNamesAsArray = PrometheusConfiguration.get().getLabeledBuildParameterNamesAsArray(); - - // Add this to the repo as well so I can group by Github Repository - String repoName = StringUtils.substringBetween(job.getFullName(), "/"); - if (repoName == null) { - repoName = NOT_AVAILABLE; - } - String[] baseLabelValueArray = {job.getFullName(), repoName, String.valueOf(job.isBuildable())}; + String[] baseLabelValueArray = JobLabel.getBaseLabelValues(job); Run lastBuild = job.getLastBuild(); // Never built @@ -233,38 +257,9 @@ protected void appendJobMetrics(Job job) { logger.debug("getting metrics for run [{}] from job [{}], include per run metrics [{}]", run.getNumber(), job.getName(), isPerBuildMetrics); if (Runs.includeBuildInMetrics(run)) { logger.debug("getting build info for run [{}] from job [{}]", run.getNumber(), job.getName()); - - Result runResult = run.getResult(); - String[] labelValueArray = baseLabelValueArray; - - if (isAppendParamLabel) { - String params = Runs.getBuildParameters(run).entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(";")); - labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); - labelValueArray[labelValueArray.length - 1] = params; - } - if (isAppendStatusLabel) { - String resultString = UNDEFINED; - if (runResult != null) { - resultString = runResult.toString(); - } - labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); - labelValueArray[labelValueArray.length - 1] = run.isBuilding() ? "RUNNING" : resultString; - } - - for (String configBuildParam : buildParameterNamesAsArray) { - labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); - String paramValue = UNDEFINED; - Object paramInBuild = Runs.getBuildParameters(run).get(configBuildParam); - if (paramInBuild != null) { - paramValue = String.valueOf(paramInBuild); - } - labelValueArray[labelValueArray.length - 1] = paramValue; - } + String[] labelValueArray = JobLabel.getJobLabelVaues(job, run); summary.calculateMetric(run, labelValueArray); - jobFailedCount.calculateMetric(run, labelValueArray); - jobSuccessCount.calculateMetric(run, labelValueArray); - if (isPerBuildMetrics) { labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); labelValueArray[labelValueArray.length - 1] = String.valueOf(run.getNumber()); diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java index 1ffa967f6..2faffb040 100644 --- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java @@ -8,11 +8,14 @@ public enum CollectorType { NODES_ONLINE_GAUGE("nodes_online"), BUILD_DURATION_GAUGE("build_duration_milliseconds"), BUILD_DURATION_SUMMARY("duration_milliseconds_summary"), - BUILD_FAILED_COUNTER("failed_build_count"), BUILD_RESULT_GAUGE("build_result"), BUILD_RESULT_ORDINAL_GAUGE("build_result_ordinal"), BUILD_START_GAUGE("build_start_time_milliseconds"), + BUILD_FAILED_COUNTER("failed_build_count"), + BUILD_TOTAL_COUNTER("total_build_count"), BUILD_SUCCESSFUL_COUNTER("success_build_count"), + BUILD_UNSTABLE_COUNTER("unstable_build_count"), + BUILD_ABORTED_COUNTER("aborted_build_count"), BUILD_LIKELY_STUCK_GAUGE("likely_stuck"), FAILED_TESTS_GAUGE("build_tests_failing"), diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildAbortedCounter.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildAbortedCounter.java new file mode 100644 index 000000000..80cb0f1dc --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildAbortedCounter.java @@ -0,0 +1,41 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import hudson.model.Result; +import hudson.model.Run; +import io.prometheus.client.Counter; +import io.prometheus.client.SimpleCollector; + +import org.jenkinsci.plugins.prometheus.collectors.CollectorType; + +public class BuildAbortedCounter extends BuildsMetricCollector, Counter> { + protected BuildAbortedCounter(String[] labelNames, String namespace, String subsystem) { + super(labelNames, namespace, subsystem); + } + + protected BuildAbortedCounter(String[] labelNames, String namespace, String subsystem, String prefix) { + super(labelNames, namespace, subsystem, prefix); + } + + @Override + protected CollectorType getCollectorType() { + return CollectorType.BUILD_ABORTED_COUNTER; + } + + @Override + protected String getHelpText() { + return "aborted build count"; + } + + @Override + protected SimpleCollector.Builder getCollectorBuilder() { + return Counter.build(); + } + + @Override + public void calculateMetric(Run jenkinsObject, String[] labelValues) { + // Increment counter if result was unstable. + if(jenkinsObject.getResult() == Result.ABORTED){ + this.collector.labels(labelValues).inc(); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java index 17c2ebb74..9888c20f5 100644 --- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java @@ -7,8 +7,6 @@ import org.jenkinsci.plugins.prometheus.collectors.MetricCollector; import org.jenkinsci.plugins.prometheus.collectors.NoOpMetricCollector; -import static org.jenkinsci.plugins.prometheus.collectors.CollectorType.*; - public class BuildCollectorFactory extends BaseCollectorFactory { public BuildCollectorFactory() { @@ -41,6 +39,12 @@ public BuildCollectorFactory() { return saveBuildCollector(new TotalTestsGauge(labelNames, namespace, subsystem, prefix)); case BUILD_LIKELY_STUCK_GAUGE: return saveBuildCollector(new BuildLikelyStuckGauge(labelNames, namespace, subsystem, prefix)); + case BUILD_ABORTED_COUNTER: + return saveBuildCollector(new BuildAbortedCounter(labelNames, namespace, subsystem, prefix)); + case BUILD_UNSTABLE_COUNTER: + return saveBuildCollector(new BuildUnstableCounter(labelNames, namespace, subsystem, prefix)); + case BUILD_TOTAL_COUNTER: + return saveBuildCollector(new BuildTotalCounter(labelNames, namespace, subsystem, prefix)); default: return new NoOpMetricCollector<>(); } diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCompletionListener.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCompletionListener.java new file mode 100644 index 000000000..e2494166a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCompletionListener.java @@ -0,0 +1,113 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import hudson.Extension; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.listeners.RunListener; + +/* + * Listens to builds that have been completed and stores them in a list. + * The JobCollector reads items in the list when it performs a scrape and + * publishes the data. + * Class extends https://javadoc.jenkins.io/hudson/model/listeners/RunListener.html + */ +public class BuildCompletionListener extends RunListener> { + // static instance of the class to use as a singleton. + private static BuildCompletionListener _Listener; + + // Lock to synchronize iteration and adding to the collection + private Lock lock; + + // Holds the list o runs in queue. + private List> runStack; + + // Iterable that defines a close method (allows us to use try resource) block + // in JobCollector.java + public interface CloseableIterator extends Iterator, AutoCloseable { + void close(); + } + + // Protected so no one can create their own copy of the class. + protected BuildCompletionListener(){ + runStack = Collections.synchronizedList(new ArrayList<>()); + lock = new ReentrantLock(); + } + + /* + * Extension tells Jenkins to register this class as a RunListener and to use + * this method in order to retrieve an instance of the class. It is a singleton + * so we can get the same reference registered in Jenkins in another class. + */ + @Extension + public synchronized static BuildCompletionListener getInstance(){ + if(_Listener == null){ + _Listener = new BuildCompletionListener(); + } + return _Listener; + } + + /* + * Fires on completion of a job. + */ + public void onCompleted(Run run, TaskListener listener){ + push(run); + } + + /* + * Pushes a run onto the list + */ + private synchronized void push(Run run){ + // Acquire lock + lock.lock(); + + // Try to add the run to the list. If something goes wrong, make sure + // we still unlock the lock! + try{ + runStack.add(run); + } + finally{ + lock.unlock(); + } + } + + /* + * Returns a closeable iterator + */ + public synchronized CloseableIterator> iterator(){ + // acquire lock before iterating + lock.lock(); + return new CloseableIterator>() { + // Get iterator from the list + private Iterator> iterator = runStack.iterator(); + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Run next() { + return iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + } + + // When we close the iterator, clear the list right before we unlock. + // This ensures we don't see the same job twice if iterator is called again. + public void close() { + runStack.clear(); + lock.unlock(); + } + }; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounter.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounter.java index 2b2bafa42..9e317064f 100644 --- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounter.java +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounter.java @@ -1,15 +1,20 @@ package org.jenkinsci.plugins.prometheus.collectors.builds; +import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import io.prometheus.client.Counter; import io.prometheus.client.SimpleCollector; + import org.jenkinsci.plugins.prometheus.collectors.CollectorType; -public class BuildFailedCounter extends BuildsMetricCollector, Counter> { +public class BuildFailedCounter extends BuildsMetricCollector, Counter> { + protected BuildFailedCounter(String[] labelNames, String namespace, String subsystem) { + super(labelNames, namespace, subsystem); + } - protected BuildFailedCounter(String[] labelNames, String namespace, String subSystem) { - super(labelNames, namespace, subSystem); + protected BuildFailedCounter(String[] labelNames, String namespace, String subsystem, String prefix) { + super(labelNames, namespace, subsystem, prefix); } @Override @@ -29,12 +34,9 @@ protected SimpleCollector.Builder getCollectorBuilder() { @Override public void calculateMetric(Run jenkinsObject, String[] labelValues) { - Result runResult = jenkinsObject.getResult(); - if (runResult != null && !jenkinsObject.isBuilding()) { - if (!runResult.equals(Result.SUCCESS) && !runResult.equals(Result.UNSTABLE)) { - this.collector.labels(labelValues).inc(); - } - + // increment counter if the build failed. + if(jenkinsObject.getResult() == Result.FAILURE){ + this.collector.labels(labelValues).inc(); } } } diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounter.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounter.java index 09ccc3fb5..83f659307 100644 --- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounter.java +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounter.java @@ -4,12 +4,16 @@ import hudson.model.Run; import io.prometheus.client.Counter; import io.prometheus.client.SimpleCollector; + import org.jenkinsci.plugins.prometheus.collectors.CollectorType; public class BuildSuccessfulCounter extends BuildsMetricCollector, Counter> { + protected BuildSuccessfulCounter(String[] labelNames, String namespace, String subsystem) { + super(labelNames, namespace, subsystem); + } - protected BuildSuccessfulCounter(String[] labelNames, String namespace, String subSystem) { - super(labelNames, namespace, subSystem); + protected BuildSuccessfulCounter(String[] labelNames, String namespace, String subsystem, String prefix) { + super(labelNames, namespace, subsystem, prefix); } @Override @@ -29,11 +33,9 @@ protected SimpleCollector.Builder getCollectorBuilder() { @Override public void calculateMetric(Run jenkinsObject, String[] labelValues) { - Result runResult = jenkinsObject.getResult(); - if (runResult != null && !jenkinsObject.isBuilding()) { - if (runResult.equals(Result.SUCCESS) || runResult.equals(Result.UNSTABLE)) { - this.collector.labels(labelValues).inc(); - } - } + // Increment the counter if the result of run was successful. + if(jenkinsObject.getResult() == Result.SUCCESS){ + this.collector.labels(labelValues).inc(); + } } } diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildTotalCounter.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildTotalCounter.java new file mode 100644 index 000000000..0d991cf12 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildTotalCounter.java @@ -0,0 +1,41 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import hudson.model.Result; +import hudson.model.Run; +import io.prometheus.client.Counter; +import io.prometheus.client.SimpleCollector; + +import org.jenkinsci.plugins.prometheus.collectors.CollectorType; + +public class BuildTotalCounter extends BuildsMetricCollector, Counter> { + protected BuildTotalCounter(String[] labelNames, String namespace, String subsystem) { + super(labelNames, namespace, subsystem); + } + + protected BuildTotalCounter(String[] labelNames, String namespace, String subsystem, String prefix) { + super(labelNames, namespace, subsystem, prefix); + } + + @Override + protected CollectorType getCollectorType() { + return CollectorType.BUILD_TOTAL_COUNTER; + } + + @Override + protected String getHelpText() { + return "Total build count"; + } + + @Override + protected SimpleCollector.Builder getCollectorBuilder() { + return Counter.build(); + } + + @Override + public void calculateMetric(Run jenkinsObject, String[] labelValues) { + // Increment counter every run that is completed. + if(jenkinsObject.getResult() != Result.NOT_BUILT){ + this.collector.labels(labelValues).inc(); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildUnstableCounter.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildUnstableCounter.java new file mode 100644 index 000000000..f734b52b0 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildUnstableCounter.java @@ -0,0 +1,41 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import hudson.model.Result; +import hudson.model.Run; +import io.prometheus.client.Counter; +import io.prometheus.client.SimpleCollector; + +import org.jenkinsci.plugins.prometheus.collectors.CollectorType; + +public class BuildUnstableCounter extends BuildsMetricCollector, Counter> { + protected BuildUnstableCounter(String[] labelNames, String namespace, String subsystem) { + super(labelNames, namespace, subsystem); + } + + protected BuildUnstableCounter(String[] labelNames, String namespace, String subsystem, String prefix) { + super(labelNames, namespace, subsystem, prefix); + } + + @Override + protected CollectorType getCollectorType() { + return CollectorType.BUILD_UNSTABLE_COUNTER; + } + + @Override + protected String getHelpText() { + return "Unstable build count"; + } + + @Override + protected SimpleCollector.Builder getCollectorBuilder() { + return Counter.build(); + } + + @Override + public void calculateMetric(Run jenkinsObject, String[] labelValues) { + // increment counter if the result was unstable. + if(jenkinsObject.getResult() == Result.UNSTABLE){ + this.collector.labels(labelValues).inc(); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildsMetricCollector.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildsMetricCollector.java index 2808bdae9..a46cea16d 100644 --- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildsMetricCollector.java +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildsMetricCollector.java @@ -4,8 +4,6 @@ import org.jenkinsci.plugins.prometheus.collectors.BaseMetricCollector; public abstract class BuildsMetricCollector> extends BaseMetricCollector { - - protected BuildsMetricCollector(String[] labelNames, String namespace, String subsystem) { super(labelNames, namespace, subsystem); } diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/CounterManager.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/CounterManager.java new file mode 100644 index 000000000..ad9c1eaba --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/CounterManager.java @@ -0,0 +1,121 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import java.util.Arrays; +import java.util.HashMap; + +import org.jenkinsci.plugins.prometheus.collectors.CollectorType; +import org.jenkinsci.plugins.prometheus.collectors.MetricCollector; + +import hudson.model.Run; +import io.prometheus.client.Collector; + +/* + * This class acts as a database to keep track of counters and return an existing counter + * if it has already been initialized. This class is necessary due to the way the plugin handles + * configuration changes. Changing the plugins configuration can cause the labels of a metric + * to change. This manager compares whether it has seen a counter with a specific label before + * and returns an existing counter if it exists. Otherwise it will return a new counter initialized at zero. + */ +public class CounterManager { + // Keeps track of Counters we have seen. + private HashMap, ? extends Collector>> registeredCounters; + + // Static singleton instance. + private static CounterManager _Manager; + + // Initialize the map + private CounterManager() { + registeredCounters = new HashMap, ? extends Collector>>(); + } + + /* + * Singleton instance method to get the manager. + */ + public static CounterManager getManager() { + if (_Manager == null) { + _Manager = new CounterManager(); + } + return _Manager; + } + + /* + Determine if we have seen the counter before + returns true if so otherwise false. + */ + private Boolean hasCounter(CounterEntry entry) { + return registeredCounters.containsKey(entry); + } + + /* + * Retrives a counter or initializes a new one if it doesn't exist + * @return Metric collector counter. + */ + public MetricCollector, ? extends Collector> getCounter(CollectorType type, String[]labels, String prefix){ + CounterEntry entry = new CounterEntry(type, labels, prefix); + + // If we have the counter return it. + if(hasCounter(entry)){ + return registeredCounters.get(entry); + } + + // Uses the build collector factory to initialize any new instances if necessary. + var factory = new BuildCollectorFactory(); + var counterCollector = factory.createCollector(type, labels, prefix); + + // Add the collector to the map + registeredCounters.put(entry, counterCollector); + return counterCollector; + } + + /* + * Holds metadata about a counter to determine if we need to create a new counter. + */ + private static class CounterEntry { + // Labels that the counter was initialized with + private String[] labels; + + // What collector type the counter is. + private CollectorType type; + + // Prefix of the counter. + private String prefix; + + /* + * Creates new counter entry + */ + public CounterEntry(CollectorType type, String[] labels, String prefix) { + this.labels = labels; + this.type = type; + this.prefix = prefix; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + + CounterEntry entry = (CounterEntry) obj; + + if(this.prefix != null && !this.prefix.equals(entry.prefix)){ + return false; + } + + if(this.type != entry.type){ + return false; + } + + // Compare labels + return Arrays.equals(labels, entry.labels); + } + + @Override + public int hashCode() { + int typeHash = type != null ? type.hashCode() : 0; + int prefixHash = prefix != null ? prefix.hashCode() : 0; + int result = 31 * (typeHash + Arrays.hashCode(labels) + prefixHash); + return result; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/JobLabel.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/JobLabel.java new file mode 100644 index 000000000..ca9702ccc --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/JobLabel.java @@ -0,0 +1,107 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.prometheus.config.PrometheusConfiguration; +import org.jenkinsci.plugins.prometheus.util.Runs; + +import hudson.model.Job; +import hudson.model.Result; +import hudson.model.Run; + +/* + * Static class that defines what labels should be added to a metric. + */ +public class JobLabel { + private static final String NOT_AVAILABLE = "NA"; + private static final String UNDEFINED = "UNDEFINED"; + + /* + * Returns the base label names based of the Prometheus configuration + * @return an array of label names + */ + public static String[] getBaseLabelNames(){ + String jobAttribute = PrometheusConfiguration.get().getJobAttributeName(); + String[] labelBaseNameArray = {jobAttribute, "repo", "buildable"}; + + return labelBaseNameArray; + } + + /* + * Returns job specific label names which appends build parameters and status + * as a possible label. + * @return array of label names with appropriate labels based off the prometheus config. + */ + public static String[] getJobLabelNames(){ + String[] labelNameArray = getBaseLabelNames(); + if (PrometheusConfiguration.get().isAppendParamLabel()) { + labelNameArray = Arrays.copyOf(labelNameArray, labelNameArray.length + 1); + labelNameArray[labelNameArray.length - 1] = "parameters"; + } + if (PrometheusConfiguration.get().isAppendStatusLabel()) { + labelNameArray = Arrays.copyOf(labelNameArray, labelNameArray.length + 1); + labelNameArray[labelNameArray.length - 1] = "status"; + } + + String[] buildParameterNamesAsArray = PrometheusConfiguration.get().getLabeledBuildParameterNamesAsArray(); + for (String buildParam : buildParameterNamesAsArray) { + labelNameArray = Arrays.copyOf(labelNameArray, labelNameArray.length + 1); + labelNameArray[labelNameArray.length - 1] = buildParam.trim(); + } + return labelNameArray; + } + + /* + * Gets the base label values of a job. Common fields between all of the metric label values. + * @return array of base labels. + */ + public static String[] getBaseLabelValues(Job job) { + // Add this to the repo as well so I can group by Github Repository + String repoName = StringUtils.substringBetween(job.getFullName(), "/"); + if (repoName == null) { + repoName = NOT_AVAILABLE; + } + String[] baseLabelValueArray = { job.getFullName(), repoName, String.valueOf(job.isBuildable()) }; + return baseLabelValueArray; + } + + /* + * Gets label values specific to job centric metrics. + * @return array of label values for a job. + */ + public static String[] getJobLabelVaues(Job job, Run run) { + boolean isAppendParamLabel = PrometheusConfiguration.get().isAppendParamLabel(); + boolean isAppendStatusLabel = PrometheusConfiguration.get().isAppendStatusLabel(); + String[] buildParameterNamesAsArray = PrometheusConfiguration.get().getLabeledBuildParameterNamesAsArray(); + + Result runResult = run.getResult(); + String[] labelValueArray = getBaseLabelValues(job); + if (isAppendParamLabel) { + String params = Runs.getBuildParameters(run).entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(";")); + labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); + labelValueArray[labelValueArray.length - 1] = params; + } + if (isAppendStatusLabel) { + String resultString = UNDEFINED; + if (runResult != null) { + resultString = runResult.toString(); + } + labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); + labelValueArray[labelValueArray.length - 1] = run.isBuilding() ? "RUNNING" : resultString; + } + + for (String configBuildParam : buildParameterNamesAsArray) { + labelValueArray = Arrays.copyOf(labelValueArray, labelValueArray.length + 1); + String paramValue = UNDEFINED; + Object paramInBuild = Runs.getBuildParameters(run).get(configBuildParam); + if (paramInBuild != null) { + paramValue = String.valueOf(paramInBuild); + } + labelValueArray[labelValueArray.length - 1] = paramValue; + } + return labelValueArray; + } +} diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildAbortedCounterTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildAbortedCounterTest.java new file mode 100644 index 000000000..4d53b34f1 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildAbortedCounterTest.java @@ -0,0 +1,100 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import hudson.model.Result; +import io.prometheus.client.Collector; +import org.jenkinsci.plugins.prometheus.collectors.testutils.MockedRunCollectorTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.mockito.Mockito.when; + +public class BuildAbortedCounterTest extends MockedRunCollectorTest { + + + @Test + public void testNothingIsIncreasedOnUnstableBuild() { + when(mock.getResult()).thenReturn(Result.UNSTABLE); + testNonFailureStateBuild(); + } + + @Test + public void testNothingIsIncreasedOnSuccessfulBuild() { + when(mock.getResult()).thenReturn(Result.SUCCESS); + testNonFailureStateBuild(); + } + + @Test + public void testNothingIsIncreasedOnNotBuiltBuild() { + when(mock.getResult()).thenReturn(Result.NOT_BUILT); + testSingleCalculation(0); + } + + @Test + public void testIncreasedOnAbortedBuild() { + when(mock.getResult()).thenReturn(Result.ABORTED); + testSingleCalculation(2); + } + + @Test + public void testNothingIsIncreasedOnBuildResultFailure() { + when(mock.getResult()).thenReturn(Result.FAILURE); + testSingleCalculation(0); + } + + private void testSingleCalculation(int expectedCount) { + BuildAbortedCounter sut = new BuildAbortedCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(expectedCount, collect.get(0).samples.size(), "Would expect one result"); + + for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { + if (sample.name.equals("default_jenkins_builds_failed_build_count_total")) { + Assertions.assertEquals(1.0, sample.value); + } + if (sample.name.equals("default_jenkins_builds_failed_build_count_created")) { + Assertions.assertTrue(sample.value > 0); + } + } + } + + @Test + public void testCounterIsNotChangedResultFailure() { + when(mock.getResult()).thenReturn(Result.FAILURE); + + BuildAbortedCounter sut = new BuildAbortedCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(0, collect.get(0).samples.size(), "Would expect one result"); + + for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { + if (sample.name.equals("default_jenkins_builds_failed_build_count_total")) { + Assertions.assertEquals(2.0, sample.value); + } + if (sample.name.equals("default_jenkins_builds_failed_build_count_created")) { + Assertions.assertTrue(sample.value > 0); + } + } + } + + private void testNonFailureStateBuild() { + BuildAbortedCounter sut = new BuildAbortedCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(0, collect.get(0).samples.size(), "Would expect one result"); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounterTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounterTest.java index 653d0d5f3..a4a0bbd67 100644 --- a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounterTest.java +++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildFailedCounterTest.java @@ -28,22 +28,22 @@ public void testNothingIsIncreasedOnSuccessfulBuild() { @Test public void testNothingIsIncreasedOnNotBuiltBuild() { when(mock.getResult()).thenReturn(Result.NOT_BUILT); - testSingleCalculation(); + testSingleCalculation(0); } @Test public void testNothingIsIncreasedOnAbortedBuild() { when(mock.getResult()).thenReturn(Result.ABORTED); - testSingleCalculation(); + testSingleCalculation(0); } @Test public void testCollectOnBuildResultFailure() { when(mock.getResult()).thenReturn(Result.FAILURE); - testSingleCalculation(); + testSingleCalculation(2); } - private void testSingleCalculation() { + private void testSingleCalculation(int expectedCount) { BuildFailedCounter sut = new BuildFailedCounter(getLabelNames(), getNamespace(), getSubSystem()); sut.calculateMetric(mock, getLabelValues()); @@ -51,7 +51,7 @@ private void testSingleCalculation() { List collect = sut.collect(); Assertions.assertEquals(1, collect.size()); - Assertions.assertEquals(2, collect.get(0).samples.size(), "Would expect one result"); + Assertions.assertEquals(expectedCount, collect.get(0).samples.size(), "Would expect one result"); for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { if (sample.name.equals("default_jenkins_builds_failed_build_count_total")) { diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounterTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounterTest.java index 797313b1b..c05e844be 100644 --- a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounterTest.java +++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildSuccessfulCounterTest.java @@ -15,13 +15,13 @@ public class BuildSuccessfulCounterTest extends MockedRunCollectorTest { @Test public void testNothingIsIncreasedOnUnstableBuild() { when(mock.getResult()).thenReturn(Result.UNSTABLE); - testSingleCalculation(); + testSingleCalculation(0); } @Test - public void testNothingIsIncreasedOnSuccessfulBuild() { + public void testIncreasedOnSuccessfulBuild() { when(mock.getResult()).thenReturn(Result.SUCCESS); - testSingleCalculation(); + testSingleCalculation(2); } @Test @@ -42,7 +42,7 @@ public void testCollectOnBuildResultFailure() { testNonSuccessStateBuild(); } - private void testSingleCalculation() { + private void testSingleCalculation(int expectedCount) { BuildSuccessfulCounter sut = new BuildSuccessfulCounter(getLabelNames(), getNamespace(), getSubSystem()); sut.calculateMetric(mock, getLabelValues()); @@ -51,7 +51,7 @@ private void testSingleCalculation() { Assertions.assertEquals(1, collect.size()); - Assertions.assertEquals(2, collect.get(0).samples.size(), "Would expect one result"); + Assertions.assertEquals(expectedCount, collect.get(0).samples.size(), "Would expect one result"); for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { if (sample.name.equals("default_jenkins_builds_success_build_count_total")) { diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildTotalCounterTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildTotalCounterTest.java new file mode 100644 index 000000000..4071ef9c1 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildTotalCounterTest.java @@ -0,0 +1,64 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import hudson.model.Result; +import io.prometheus.client.Collector; +import org.jenkinsci.plugins.prometheus.collectors.testutils.MockedRunCollectorTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.mockito.Mockito.when; + +public class BuildTotalCounterTest extends MockedRunCollectorTest { + + @Test + public void testIncreasedOnUnstableBuild() { + when(mock.getResult()).thenReturn(Result.UNSTABLE); + testSingleCalculation(2); + } + + @Test + public void tesIncreasedOnSuccessfulBuild() { + when(mock.getResult()).thenReturn(Result.SUCCESS); + testSingleCalculation(2); + } + + @Test + public void testNothingIsIncreasedOnNotBuiltBuild() { + when(mock.getResult()).thenReturn(Result.NOT_BUILT); + testSingleCalculation(0); + } + + @Test + public void testIncreasedOnAbortedBuild() { + when(mock.getResult()).thenReturn(Result.ABORTED); + testSingleCalculation(2); + } + + @Test + public void testIncreasedOnBuildResultFailure() { + when(mock.getResult()).thenReturn(Result.FAILURE); + testSingleCalculation(2); + } + + private void testSingleCalculation(int expectedCount) { + BuildTotalCounter sut = new BuildTotalCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(expectedCount, collect.get(0).samples.size(), "Would expect one result"); + + for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { + if (sample.name.equals("default_jenkins_builds_failed_build_count_total")) { + Assertions.assertEquals(1.0, sample.value); + } + if (sample.name.equals("default_jenkins_builds_failed_build_count_created")) { + Assertions.assertTrue(sample.value > 0); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildUnstableCounterTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildUnstableCounterTest.java new file mode 100644 index 000000000..b17a71671 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildUnstableCounterTest.java @@ -0,0 +1,100 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import hudson.model.Result; +import io.prometheus.client.Collector; +import org.jenkinsci.plugins.prometheus.collectors.testutils.MockedRunCollectorTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.mockito.Mockito.when; + +public class BuildUnstableCounterTest extends MockedRunCollectorTest { + + + @Test + public void testIncreasedOnUnstableBuild() { + when(mock.getResult()).thenReturn(Result.UNSTABLE); + testSingleCalculation(2); + } + + @Test + public void testNothingIsIncreasedOnSuccessfulBuild() { + when(mock.getResult()).thenReturn(Result.SUCCESS); + testNonFailureStateBuild(); + } + + @Test + public void testNothingIsIncreasedOnNotBuiltBuild() { + when(mock.getResult()).thenReturn(Result.NOT_BUILT); + testSingleCalculation(0); + } + + @Test + public void testNoIncreaseOnAbortedBuild() { + when(mock.getResult()).thenReturn(Result.ABORTED); + testSingleCalculation(0); + } + + @Test + public void testNothingIsIncreasedOnBuildResultFailure() { + when(mock.getResult()).thenReturn(Result.FAILURE); + testSingleCalculation(0); + } + + private void testSingleCalculation(int expectedCount) { + BuildUnstableCounter sut = new BuildUnstableCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(expectedCount, collect.get(0).samples.size(), "Would expect one result"); + + for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { + if (sample.name.equals("default_jenkins_builds_failed_build_count_total")) { + Assertions.assertEquals(1.0, sample.value); + } + if (sample.name.equals("default_jenkins_builds_failed_build_count_created")) { + Assertions.assertTrue(sample.value > 0); + } + } + } + + @Test + public void testCounterIsNotChangedResultFailure() { + when(mock.getResult()).thenReturn(Result.FAILURE); + + BuildUnstableCounter sut = new BuildUnstableCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(0, collect.get(0).samples.size(), "Would expect one result"); + + for (Collector.MetricFamilySamples.Sample sample : collect.get(0).samples) { + if (sample.name.equals("default_jenkins_builds_failed_build_count_total")) { + Assertions.assertEquals(2.0, sample.value); + } + if (sample.name.equals("default_jenkins_builds_failed_build_count_created")) { + Assertions.assertTrue(sample.value > 0); + } + } + } + + private void testNonFailureStateBuild() { + BuildUnstableCounter sut = new BuildUnstableCounter(getLabelNames(), getNamespace(), getSubSystem()); + + sut.calculateMetric(mock, getLabelValues()); + + List collect = sut.collect(); + + Assertions.assertEquals(1, collect.size()); + Assertions.assertEquals(0, collect.get(0).samples.size(), "Would expect one result"); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/CounterManagerTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/CounterManagerTest.java new file mode 100644 index 000000000..a35252789 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/CounterManagerTest.java @@ -0,0 +1,92 @@ +package org.jenkinsci.plugins.prometheus.collectors.builds; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.jenkinsci.plugins.prometheus.collectors.CollectorType; +import org.jenkinsci.plugins.prometheus.collectors.testutils.MockedRunCollectorTest; +import org.jenkinsci.plugins.prometheus.config.PrometheusConfiguration; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class CounterManagerTest extends MockedRunCollectorTest { + private CounterManager manager; + + public CounterManagerTest() { + manager = CounterManager.getManager(); + } + + @Test + public void TestEquivalentEntryReturnsCounter() { + String[] labels = new String[] { "TestLabel" }; + + try (MockedStatic configStatic = mockStatic(PrometheusConfiguration.class)) { + PrometheusConfiguration config = mock(PrometheusConfiguration.class); + when(config.getDefaultNamespace()).thenReturn(getNamespace()); + configStatic.when(PrometheusConfiguration::get).thenReturn(config); + var retrievedCounter = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, labels, null); + var retrievedCounter2 = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, labels, null); + + // Should be a value reference comparison. They should be the exact same + // MetricCollector. + Assert.assertEquals(retrievedCounter, retrievedCounter2); + } + } + + @Test + public void TestLabelChangeReturnsNewCounter(){ + String[] label1 = new String[]{"labels"}; + String[] label2 = Arrays.copyOf(label1, 2); + label2[1] = "Hi"; + try (MockedStatic configStatic = mockStatic(PrometheusConfiguration.class)) { + PrometheusConfiguration config = getMockCounterManagerConfig(); + configStatic.when(PrometheusConfiguration::get).thenReturn(config); + var retrievedCounter = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, label1, null); + var retrievedCounter2 = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, label2, null); + + // Should be a value reference comparison. They should be different since labels differ. + Assert.assertNotEquals(retrievedCounter, retrievedCounter2); + } + } + + @Test + public void TestPrefixChangeReturnsNewCounter(){ + String[] label1 = new String[]{"labels"}; + + try (MockedStatic configStatic = mockStatic(PrometheusConfiguration.class)) { + PrometheusConfiguration config = getMockCounterManagerConfig(); + configStatic.when(PrometheusConfiguration::get).thenReturn(config); + var retrievedCounter = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, label1, "yes"); + var retrievedCounter2 = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, label1, null); + + // Should be a value reference comparison. They should not be the same since prefix changed + Assert.assertNotEquals(retrievedCounter, retrievedCounter2); + } + } + + @Test + public void TestDifferentCounterReturnsUniqueCounter(){ + String[] label1 = new String[]{"labels"}; + + try (MockedStatic configStatic = mockStatic(PrometheusConfiguration.class)) { + PrometheusConfiguration config = getMockCounterManagerConfig(); + configStatic.when(PrometheusConfiguration::get).thenReturn(config); + var retrievedCounter = manager.getCounter(CollectorType.BUILD_ABORTED_COUNTER, label1, null); + var retrievedCounter2 = manager.getCounter(CollectorType.BUILD_SUCCESSFUL_COUNTER, label1, null); + + // Should be a value reference comparison. They should not be the same since the collector type differs. + Assert.assertNotEquals(retrievedCounter, retrievedCounter2); + } + } + + public PrometheusConfiguration getMockCounterManagerConfig(){ + PrometheusConfiguration config = mock(PrometheusConfiguration.class); + when(config.getDefaultNamespace()).thenReturn(getNamespace()); + + return config; + } +}