001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
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 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
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 io.jboot.support.metric.reporter.prometheus;
017
018import com.codahale.metrics.Timer;
019import com.codahale.metrics.*;
020import io.jboot.Jboot;
021import io.jboot.app.JbootApplicationConfig;
022import io.jboot.utils.ClassUtil;
023import io.jboot.utils.NetUtil;
024import io.jboot.utils.StrUtil;
025import io.prometheus.client.dropwizard.DropwizardExports;
026import io.prometheus.client.dropwizard.samplebuilder.DefaultSampleBuilder;
027import io.prometheus.client.dropwizard.samplebuilder.SampleBuilder;
028
029import java.util.*;
030import java.util.concurrent.TimeUnit;
031import java.util.logging.Level;
032import java.util.logging.Logger;
033
034public class PrometheusExports extends io.prometheus.client.Collector implements io.prometheus.client.Collector.Describable {
035    private static final Logger LOGGER = Logger.getLogger(DropwizardExports.class.getName());
036    private MetricRegistry registry;
037    private SampleBuilder sampleBuilder;
038    private JbootApplicationConfig appConfig = Jboot.config(JbootApplicationConfig.class);
039
040    /**
041     * Creates a new DropwizardExports with a {@link DefaultSampleBuilder}.
042     *
043     * @param registry a metric registry to export in prometheus.
044     */
045    public PrometheusExports(MetricRegistry registry) {
046        this.registry = registry;
047        this.sampleBuilder = new DefaultSampleBuilder();
048    }
049
050    /**
051     * @param registry      a metric registry to export in prometheus.
052     * @param sampleBuilder sampleBuilder to use to create prometheus samples.
053     */
054    public PrometheusExports(MetricRegistry registry, SampleBuilder sampleBuilder) {
055        this.registry = registry;
056        this.sampleBuilder = sampleBuilder;
057    }
058
059    private static String getHelpMessage(String metricName, Metric metric) {
060        return String.format("Generated from Jboot metric import (metric=%s, type=%s)",
061                metricName, ClassUtil.getUsefulClass(metric.getClass()).getName());
062    }
063
064    // List<String> additionalLabelNames, List<String> additionalLabelValues,
065    private List<String> getDefaultAdditionalLabelNames(String... names) {
066        List<String> array = getDefaultAdditionalLabelNames();
067        array.addAll(Arrays.asList(names));
068        return array;
069    }
070
071
072    private List<String> getDefaultAdditionalLabelNames() {
073        List<String> newArray = new ArrayList<>();
074        newArray.add("application");
075        newArray.add("instance");
076        return newArray;
077    }
078
079
080    private List<String> getDefaultAdditionalLabelValues(String... values) {
081        List<String> array = getDefaultAdditionalLabelValues();
082        array.addAll(Arrays.asList(values));
083        return array;
084    }
085
086
087    private List<String> getDefaultAdditionalLabelValues() {
088        List<String> newArray = new ArrayList<>();
089        newArray.add(appConfig.getName());
090        newArray.add(getInstance());
091        return newArray;
092    }
093
094    private static String instance = null;
095
096    private static final String getInstance() {
097        if (instance == null) {
098            String ipAddress = NetUtil.getLocalIpAddress();
099            if (StrUtil.isNotBlank(ipAddress)) {
100                instance = ipAddress + ":" + Jboot.configValue("undertow.port");
101            } else {
102                instance = "";
103            }
104        }
105        return instance;
106    }
107
108
109    /**
110     * Export counter as Prometheus <a href="https://prometheus.io/docs/concepts/metric_types/#gauge">Gauge</a>.
111     */
112    MetricFamilySamples fromCounter(String dropwizardName, Counter counter) {
113        MetricFamilySamples.Sample sample = sampleBuilder.createSample(dropwizardName, ""
114                , getDefaultAdditionalLabelNames()
115                , getDefaultAdditionalLabelValues()
116                , new Long(counter.getCount()).doubleValue());
117        return new MetricFamilySamples(sample.name, Type.GAUGE, getHelpMessage(dropwizardName, counter), Arrays.asList(sample));
118    }
119
120    /**
121     * Export gauge as a prometheus gauge.
122     */
123    MetricFamilySamples fromGauge(String dropwizardName, Gauge gauge) {
124        Object obj = gauge.getValue();
125        double value;
126        if (obj instanceof Number) {
127            value = ((Number) obj).doubleValue();
128        } else if (obj instanceof Boolean) {
129            value = ((Boolean) obj) ? 1 : 0;
130        } else {
131            LOGGER.log(Level.FINE, String.format("Invalid type for Gauge %s: %s", sanitizeMetricName(dropwizardName),
132                    obj == null ? "null" : obj.getClass().getName()));
133            return null;
134        }
135        MetricFamilySamples.Sample sample = sampleBuilder.createSample(dropwizardName, ""
136                , getDefaultAdditionalLabelNames()
137                , getDefaultAdditionalLabelValues()
138                , value);
139        return new MetricFamilySamples(sample.name, Type.GAUGE, getHelpMessage(dropwizardName, gauge), Arrays.asList(sample));
140    }
141
142    /**
143     * Export a histogram snapshot as a prometheus SUMMARY.
144     *
145     * @param dropwizardName metric name.
146     * @param snapshot       the histogram snapshot.
147     * @param count          the total sample count for this snapshot.
148     * @param factor         a factor to apply to histogram values.
149     */
150    MetricFamilySamples fromSnapshotAndCount(String dropwizardName, Snapshot snapshot, long count, double factor, String helpMessage) {
151        long sum = 0;
152        long[] values = snapshot.getValues();
153        if (values != null){
154            for (long value : values) {
155                sum += value;
156            }
157        }
158        List<MetricFamilySamples.Sample> samples = Arrays.asList(
159                sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.5"), snapshot.getMedian() * factor),
160                sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.75"), snapshot.get75thPercentile() * factor),
161                sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.95"), snapshot.get95thPercentile() * factor),
162                sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.98"), snapshot.get98thPercentile() * factor),
163                sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.99"), snapshot.get99thPercentile() * factor),
164                sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.999"), snapshot.get999thPercentile() * factor),
165                sampleBuilder.createSample(dropwizardName, "_count"
166                        , getDefaultAdditionalLabelNames()
167                        , getDefaultAdditionalLabelValues()
168                        , count),
169                sampleBuilder.createSample(dropwizardName, "_sum"
170                        , getDefaultAdditionalLabelNames()
171                        , getDefaultAdditionalLabelValues()
172                        , sum),
173                sampleBuilder.createSample(dropwizardName, "_mean"
174                        , getDefaultAdditionalLabelNames()
175                        , getDefaultAdditionalLabelValues()
176                        , snapshot.getMean()),
177                sampleBuilder.createSample(dropwizardName, "_median"
178                        , getDefaultAdditionalLabelNames()
179                        , getDefaultAdditionalLabelValues()
180                        , snapshot.getMedian()),
181                sampleBuilder.createSample(dropwizardName, "_min"
182                        , getDefaultAdditionalLabelNames()
183                        , getDefaultAdditionalLabelValues()
184                        , snapshot.getMin()),
185                sampleBuilder.createSample(dropwizardName, "_max"
186                        , getDefaultAdditionalLabelNames()
187                        , getDefaultAdditionalLabelValues()
188                        , snapshot.getMax())
189        );
190        return new MetricFamilySamples(samples.get(0).name, Type.SUMMARY, helpMessage, samples);
191    }
192
193    /**
194     * Convert histogram snapshot.
195     */
196    MetricFamilySamples fromHistogram(String dropwizardName, Histogram histogram) {
197        return fromSnapshotAndCount(dropwizardName, histogram.getSnapshot(), histogram.getCount(), 1.0,
198                getHelpMessage(dropwizardName, histogram));
199    }
200
201    /**
202     * Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit.
203     */
204    MetricFamilySamples fromTimer(String dropwizardName, Timer timer) {
205        return fromSnapshotAndCount(dropwizardName, timer.getSnapshot(), timer.getCount(),
206                1.0D / TimeUnit.SECONDS.toNanos(1L), getHelpMessage(dropwizardName, timer));
207    }
208
209    /**
210     * Export a Meter as as prometheus COUNTER.
211     */
212    MetricFamilySamples fromMeter(String dropwizardName, Meter meter) {
213        final MetricFamilySamples.Sample sample = sampleBuilder.createSample(dropwizardName, "_total"
214                , getDefaultAdditionalLabelNames()
215                , getDefaultAdditionalLabelValues()
216                , meter.getCount());
217        return new MetricFamilySamples(sample.name, Type.COUNTER, getHelpMessage(dropwizardName, meter),
218                Arrays.asList(sample));
219    }
220
221    @Override
222    public List<MetricFamilySamples> collect() {
223        Map<String, MetricFamilySamples> mfSamplesMap = new HashMap<String, MetricFamilySamples>();
224
225        for (SortedMap.Entry<String, Gauge> entry : registry.getGauges().entrySet()) {
226            addToMap(mfSamplesMap, fromGauge(entry.getKey(), entry.getValue()));
227        }
228        for (SortedMap.Entry<String, Counter> entry : registry.getCounters().entrySet()) {
229            addToMap(mfSamplesMap, fromCounter(entry.getKey(), entry.getValue()));
230        }
231        for (SortedMap.Entry<String, Histogram> entry : registry.getHistograms().entrySet()) {
232            addToMap(mfSamplesMap, fromHistogram(entry.getKey(), entry.getValue()));
233        }
234        for (SortedMap.Entry<String, Timer> entry : registry.getTimers().entrySet()) {
235            addToMap(mfSamplesMap, fromTimer(entry.getKey(), entry.getValue()));
236        }
237        for (SortedMap.Entry<String, Meter> entry : registry.getMeters().entrySet()) {
238            addToMap(mfSamplesMap, fromMeter(entry.getKey(), entry.getValue()));
239        }
240        return new ArrayList<MetricFamilySamples>(mfSamplesMap.values());
241    }
242
243    private void addToMap(Map<String, MetricFamilySamples> mfSamplesMap, MetricFamilySamples newMfSamples) {
244        if (newMfSamples != null) {
245            MetricFamilySamples currentMfSamples = mfSamplesMap.get(newMfSamples.name);
246            if (currentMfSamples == null) {
247                mfSamplesMap.put(newMfSamples.name, newMfSamples);
248            } else {
249                List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(currentMfSamples.samples);
250                samples.addAll(newMfSamples.samples);
251                mfSamplesMap.put(newMfSamples.name, new MetricFamilySamples(newMfSamples.name, currentMfSamples.type, currentMfSamples.help, samples));
252            }
253        }
254    }
255
256    @Override
257    public List<MetricFamilySamples> describe() {
258        return new ArrayList<MetricFamilySamples>();
259    }
260}