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

org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.zookeeper.metrics.prometheus;

import io.prometheus.client.Collector;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.MetricsServlet;
import io.prometheus.client.hotspot.DefaultExports;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.zookeeper.metrics.Counter;
import org.apache.zookeeper.metrics.Gauge;
import org.apache.zookeeper.metrics.MetricsContext;
import org.apache.zookeeper.metrics.MetricsProvider;
import org.apache.zookeeper.metrics.MetricsProviderLifeCycleException;
import org.apache.zookeeper.metrics.Summary;
import org.apache.zookeeper.metrics.SummarySet;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Metrics Provider implementation based on https://prometheus.io.
 *
 * @since 3.6.0
 */
public class PrometheusMetricsProvider implements MetricsProvider {

    private static final Logger LOG = LoggerFactory.getLogger(PrometheusMetricsProvider.class);
    private static final String LABEL = "key";
    private static final String[] LABELS = {LABEL};
    /**
     * We are using the 'defaultRegistry'.
     * 

* When you are running ZooKeeper (server or client) together with other * libraries every metrics will be expected as a single view. *

*/ private final CollectorRegistry collectorRegistry = CollectorRegistry.defaultRegistry; private int port = 7000; private boolean exportJvmInfo = true; private Server server; private final MetricsServletImpl servlet = new MetricsServletImpl(); private final Context rootContext = new Context(); @Override public void configure(Properties configuration) throws MetricsProviderLifeCycleException { LOG.info("Initializing metrics, configuration: {}", configuration); this.port = Integer.parseInt(configuration.getProperty("httpPort", "7000")); this.exportJvmInfo = Boolean.parseBoolean(configuration.getProperty("exportJvmInfo", "true")); } @Override public void start() throws MetricsProviderLifeCycleException { try { LOG.info("Starting /metrics HTTP endpoint at port {} exportJvmInfo: {}", port, exportJvmInfo); if (exportJvmInfo) { DefaultExports.initialize(); } server = new Server(port); ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/"); server.setHandler(context); context.addServlet(new ServletHolder(servlet), "/metrics"); server.start(); } catch (Exception err) { LOG.error("Cannot start /metrics server", err); if (server != null) { try { server.stop(); } catch (Exception suppressed) { err.addSuppressed(suppressed); } finally { server = null; } } throw new MetricsProviderLifeCycleException(err); } } // for tests MetricsServletImpl getServlet() { return servlet; } @Override public MetricsContext getRootContext() { return rootContext; } @Override public void stop() { if (server != null) { try { server.stop(); } catch (Exception err) { LOG.error("Cannot safely stop Jetty server", err); } finally { server = null; } } } /** * Dump all values to the 4lw interface and to the Admin server. *

* This method is not expected to be used to serve metrics to Prometheus. We * are using the MetricsServlet provided by Prometheus for that, leaving the * real representation to the Prometheus Java client. *

* * @param sink the receiver of data (4lw interface, Admin server or tests) */ @Override public void dump(BiConsumer sink) { sampleGauges(); Enumeration samplesFamilies = collectorRegistry.metricFamilySamples(); while (samplesFamilies.hasMoreElements()) { Collector.MetricFamilySamples samples = samplesFamilies.nextElement(); samples.samples.forEach(sample -> { String key = buildKeyForDump(sample); sink.accept(key, sample.value); }); } } private static String buildKeyForDump(Collector.MetricFamilySamples.Sample sample) { StringBuilder keyBuilder = new StringBuilder(); keyBuilder.append(sample.name); if (sample.labelNames.size() > 0) { keyBuilder.append('{'); for (int i = 0; i < sample.labelNames.size(); ++i) { if (i > 0) { keyBuilder.append(','); } keyBuilder.append(sample.labelNames.get(i)); keyBuilder.append("=\""); keyBuilder.append(sample.labelValues.get(i)); keyBuilder.append('"'); } keyBuilder.append('}'); } return keyBuilder.toString(); } /** * Update Gauges. In ZooKeeper Metrics API Gauges are callbacks served by * internal components and the value is not held by Prometheus structures. */ private void sampleGauges() { rootContext.gauges.values() .forEach(PrometheusGaugeWrapper::sample); } @Override public void resetAllValues() { // not supported on Prometheus } private class Context implements MetricsContext { private final ConcurrentMap gauges = new ConcurrentHashMap<>(); private final ConcurrentMap counters = new ConcurrentHashMap<>(); private final ConcurrentMap basicSummaries = new ConcurrentHashMap<>(); private final ConcurrentMap summaries = new ConcurrentHashMap<>(); private final ConcurrentMap basicSummarySets = new ConcurrentHashMap<>(); private final ConcurrentMap summarySets = new ConcurrentHashMap<>(); @Override public MetricsContext getContext(String name) { // no hierarchy yet return this; } @Override public Counter getCounter(String name) { return counters.computeIfAbsent(name, PrometheusCounter::new); } /** * Gauges may go up and down, in ZooKeeper they are a way to export * internal values with a callback. * * @param name the name of the gauge * @param gauge the callback */ @Override public void registerGauge(String name, Gauge gauge) { Objects.requireNonNull(name); gauges.compute(name, (id, prev) -> new PrometheusGaugeWrapper(id, gauge, prev != null ? prev.inner : null)); } @Override public void unregisterGauge(String name) { PrometheusGaugeWrapper existing = gauges.remove(name); if (existing != null) { existing.unregister(); } } @Override public Summary getSummary(String name, DetailLevel detailLevel) { if (detailLevel == DetailLevel.BASIC) { return basicSummaries.computeIfAbsent(name, (n) -> { if (summaries.containsKey(n)) { throw new IllegalArgumentException("Already registered a non basic summary as " + n); } return new PrometheusSummary(name, detailLevel); }); } else { return summaries.computeIfAbsent(name, (n) -> { if (basicSummaries.containsKey(n)) { throw new IllegalArgumentException("Already registered a basic summary as " + n); } return new PrometheusSummary(name, detailLevel); }); } } @Override public SummarySet getSummarySet(String name, DetailLevel detailLevel) { if (detailLevel == DetailLevel.BASIC) { return basicSummarySets.computeIfAbsent(name, (n) -> { if (summarySets.containsKey(n)) { throw new IllegalArgumentException("Already registered a non basic summary set as " + n); } return new PrometheusLabelledSummary(name, detailLevel); }); } else { return summarySets.computeIfAbsent(name, (n) -> { if (basicSummarySets.containsKey(n)) { throw new IllegalArgumentException("Already registered a basic summary set as " + n); } return new PrometheusLabelledSummary(name, detailLevel); }); } } } private class PrometheusCounter implements Counter { private final io.prometheus.client.Counter inner; private final String name; public PrometheusCounter(String name) { this.name = name; this.inner = io.prometheus.client.Counter .build(name, name) .register(collectorRegistry); } @Override public void add(long delta) { try { inner.inc(delta); } catch (IllegalArgumentException err) { LOG.error("invalid delta {} for metric {}", delta, name, err); } } @Override public long get() { // this method is used only for tests // Prometheus returns a "double" // it is safe to fine to a long // we are never setting non-integer values return (long) inner.get(); } } private class PrometheusGaugeWrapper { private final io.prometheus.client.Gauge inner; private final Gauge gauge; private final String name; public PrometheusGaugeWrapper(String name, Gauge gauge, io.prometheus.client.Gauge prev) { this.name = name; this.gauge = gauge; this.inner = prev != null ? prev : io.prometheus.client.Gauge .build(name, name) .register(collectorRegistry); } /** * Call the callack and update Prometheus Gauge. This method is called * when the server is polling for a value. */ private void sample() { Number value = gauge.get(); this.inner.set(value != null ? value.doubleValue() : 0); } private void unregister() { collectorRegistry.unregister(inner); } } private class PrometheusSummary implements Summary { private final io.prometheus.client.Summary inner; private final String name; public PrometheusSummary(String name, MetricsContext.DetailLevel level) { this.name = name; if (level == MetricsContext.DetailLevel.ADVANCED) { this.inner = io.prometheus.client.Summary .build(name, name) .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error .quantile(0.9, 0.01) // Add 90th percentile with 1% tolerated error .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error .register(collectorRegistry); } else { this.inner = io.prometheus.client.Summary .build(name, name) .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error .register(collectorRegistry); } } @Override public void add(long delta) { try { inner.observe(delta); } catch (IllegalArgumentException err) { LOG.error("invalid delta {} for metric {}", delta, name, err); } } } private class PrometheusLabelledSummary implements SummarySet { private final io.prometheus.client.Summary inner; private final String name; public PrometheusLabelledSummary(String name, MetricsContext.DetailLevel level) { this.name = name; if (level == MetricsContext.DetailLevel.ADVANCED) { this.inner = io.prometheus.client.Summary .build(name, name) .labelNames(LABELS) .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error .quantile(0.9, 0.01) // Add 90th percentile with 1% tolerated error .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error .register(collectorRegistry); } else { this.inner = io.prometheus.client.Summary .build(name, name) .labelNames(LABELS) .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error .register(collectorRegistry); } } @Override public void add(String key, long value) { try { inner.labels(key).observe(value); } catch (IllegalArgumentException err) { LOG.error("invalid value {} for metric {} with key {}", value, name, key, err); } } } class MetricsServletImpl extends MetricsServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // little trick: update the Gauges before serving data // from Prometheus CollectorRegistry sampleGauges(); // serve data using Prometheus built in client. super.doGet(req, resp); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy