001package io.prometheus.metrics.core.metrics;
002
003import io.prometheus.metrics.config.PrometheusProperties;
004import io.prometheus.metrics.model.snapshots.InfoSnapshot;
005import io.prometheus.metrics.model.snapshots.Labels;
006import io.prometheus.metrics.model.snapshots.Unit;
007
008import java.util.ArrayList;
009import java.util.Collections;
010import java.util.List;
011import java.util.Set;
012import java.util.concurrent.CopyOnWriteArraySet;
013
014/**
015 * Info metric. Example:
016 * <pre>{@code
017 * Info info = Info.builder()
018 *         .name("java_runtime_info")
019 *         .help("Java runtime info")
020 *         .labelNames("env", "version", "vendor", "runtime")
021 *         .register();
022 *
023 * String version = System.getProperty("java.runtime.version", "unknown");
024 * String vendor = System.getProperty("java.vm.vendor", "unknown");
025 * String runtime = System.getProperty("java.runtime.name", "unknown");
026 *
027 * info.addLabelValues("prod", version, vendor, runtime);
028 * info.addLabelValues("dev", version, vendor, runtime);
029 * }</pre>
030 */
031public class Info extends MetricWithFixedMetadata {
032
033    private final Set<Labels> labels = new CopyOnWriteArraySet<>();
034
035    private Info(Builder builder) {
036        super(builder);
037    }
038
039    /**
040     * Set the info label values. This will replace any previous values,
041     * i.e. the info metric will only have one data point after calling setLabelValues().
042     * This is good for a metric like {@code target_info} where you want only one single data point.
043     */
044    public void setLabelValues(String... labelValues) {
045        if (labelValues.length != labelNames.length) {
046            throw new IllegalArgumentException(getClass().getSimpleName() + " " + getMetadata().getName() + " was created with " + labelNames.length + " label names, but you called setLabelValues() with " + labelValues.length + " label values.");
047        }
048        Labels newLabels = Labels.of(labelNames, labelValues);
049        labels.add(newLabels);
050        labels.retainAll(Collections.singletonList(newLabels));
051    }
052
053    /**
054     * Create an info data point with the given label values.
055     */
056    public void addLabelValues(String... labelValues) {
057        if (labelValues.length != labelNames.length) {
058            throw new IllegalArgumentException(getClass().getSimpleName() + " " + getMetadata().getName() + " was created with " + labelNames.length + " label names, but you called addLabelValues() with " + labelValues.length + " label values.");
059        }
060        labels.add(Labels.of(labelNames, labelValues));
061    }
062
063    /**
064     * Remove the data point with the specified label values.
065     */
066    public void remove(String... labelValues) {
067        if (labelValues.length != labelNames.length) {
068            throw new IllegalArgumentException(getClass().getSimpleName() + " " + getMetadata().getName() + " was created with " + labelNames.length + " label names, but you called remove() with " + labelValues.length + " label values.");
069        }
070        Labels toBeRemoved = Labels.of(labelNames, labelValues);
071        labels.remove(toBeRemoved);
072    }
073
074    /**
075     * {@inheritDoc}
076     */
077    @Override
078    public InfoSnapshot collect() {
079        List<InfoSnapshot.InfoDataPointSnapshot> data = new ArrayList<>(labels.size());
080        if (labelNames.length == 0) {
081            data.add(new InfoSnapshot.InfoDataPointSnapshot(constLabels));
082        } else {
083            for (Labels label : labels) {
084                data.add(new InfoSnapshot.InfoDataPointSnapshot(label.merge(constLabels)));
085            }
086        }
087        return new InfoSnapshot(getMetadata(), data);
088    }
089
090    public static Builder builder() {
091        return new Builder(PrometheusProperties.get());
092    }
093
094    public static Builder builder(PrometheusProperties config) {
095        return new Builder(config);
096    }
097
098    public static class Builder extends MetricWithFixedMetadata.Builder<Builder, Info> {
099
100        private Builder(PrometheusProperties config) {
101            super(Collections.emptyList(), config);
102        }
103
104        /**
105         * The {@code _info} suffix will automatically be appended if it's missing.
106         * <pre>{@code
107         * Info info1 = Info.builder()
108         *     .name("runtime_info")
109         *     .build();
110         * Info info2 = Info.builder()
111         *     .name("runtime")
112         *     .build();
113         * }</pre>
114         * In the example above both {@code info1} and {@code info2} will be named {@code "runtime_info"} in Prometheus.
115         * <p>
116         * Throws an {@link IllegalArgumentException} if
117         * {@link io.prometheus.metrics.model.snapshots.PrometheusNaming#isValidMetricName(String) MetricMetadata.isValidMetricName(name)}
118         * is {@code false}.
119         */
120        @Override
121        public Builder name(String name) {
122            return super.name(stripInfoSuffix(name));
123        }
124
125        /**
126         * Throws an {@link UnsupportedOperationException} because Info metrics cannot have a unit.
127         */
128        @Override
129        public Builder unit(Unit unit) {
130            if (unit != null) {
131                throw new UnsupportedOperationException("Info metrics cannot have a unit.");
132            }
133            return this;
134        }
135
136        private static String stripInfoSuffix(String name) {
137            if (name != null && (name.endsWith("_info") || name.endsWith(".info"))) {
138                name = name.substring(0, name.length() - 5);
139            }
140            return name;
141        }
142
143        @Override
144        public Info build() {
145            return new Info(this);
146        }
147
148        @Override
149        protected Builder self() {
150            return this;
151        }
152    }
153}