/* * 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.flink.metrics.statsd; import org.apache.flink.annotation.PublicEvolving; import org.apache.flink.metrics.Counter; import org.apache.flink.metrics.Gauge; import org.apache.flink.metrics.Histogram; import org.apache.flink.metrics.HistogramStatistics; import org.apache.flink.metrics.Meter; import org.apache.flink.metrics.MetricConfig; import org.apache.flink.metrics.reporter.AbstractReporter; import org.apache.flink.metrics.reporter.Scheduled; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.util.ConcurrentModificationException; import java.util.Map; import java.util.NoSuchElementException; /** * Largely based on the StatsDReporter class by ReadyTalk * https://github.com/ReadyTalk/metrics-statsd/blob/master/metrics3-statsd/src/main/java/com/readytalk/metrics/StatsDReporter.java * * Ported since it was not present in maven central. */ @PublicEvolving public class StatsDReporter extends AbstractReporter implements Scheduled { private static final Logger LOG = LoggerFactory.getLogger(StatsDReporter.class); public static final String ARG_HOST = "host"; public static final String ARG_PORT = "port"; // public static final String ARG_CONVERSION_RATE = "rateConversion"; // public static final String ARG_CONVERSION_DURATION = "durationConversion"; private boolean closed = false; private DatagramSocket socket; private InetSocketAddress address; @Override public void open(MetricConfig config) { String host = config.getString(ARG_HOST, null); int port = config.getInteger(ARG_PORT, -1); if (host == null || host.length() == 0 || port < 1) { throw new IllegalArgumentException("Invalid host/port configuration. Host: " + host + " Port: " + port); } this.address = new InetSocketAddress(host, port); // String conversionRate = config.getString(ARG_CONVERSION_RATE, "SECONDS"); // String conversionDuration = config.getString(ARG_CONVERSION_DURATION, "MILLISECONDS"); // this.rateFactor = TimeUnit.valueOf(conversionRate).toSeconds(1); // this.durationFactor = 1.0 / TimeUnit.valueOf(conversionDuration).toNanos(1); try { this.socket = new DatagramSocket(0); } catch (SocketException e) { throw new RuntimeException("Could not create datagram socket. ", e); } log.info("Configured StatsDReporter with {host:{}, port:{}}", host, port); } @Override public void close() { closed = true; if (socket != null && !socket.isClosed()) { socket.close(); } } // ------------------------------------------------------------------------ @Override public void report() { // instead of locking here, we tolerate exceptions // we do this to prevent holding the lock for very long and blocking // operator creation and shutdown try { for (Map.Entry<Gauge<?>, String> entry : gauges.entrySet()) { if (closed) { return; } reportGauge(entry.getValue(), entry.getKey()); } for (Map.Entry<Counter, String> entry : counters.entrySet()) { if (closed) { return; } reportCounter(entry.getValue(), entry.getKey()); } for (Map.Entry<Histogram, String> entry : histograms.entrySet()) { reportHistogram(entry.getValue(), entry.getKey()); } for (Map.Entry<Meter, String> entry : meters.entrySet()) { reportMeter(entry.getValue(), entry.getKey()); } } catch (ConcurrentModificationException | NoSuchElementException e) { // ignore - may happen when metrics are concurrently added or removed // report next time } } // ------------------------------------------------------------------------ private void reportCounter(final String name, final Counter counter) { send(name, String.valueOf(counter.getCount())); } private void reportGauge(final String name, final Gauge<?> gauge) { Object value = gauge.getValue(); if (value != null) { send(name, value.toString()); } } private void reportHistogram(final String name, final Histogram histogram) { if (histogram != null) { HistogramStatistics statistics = histogram.getStatistics(); if (statistics != null) { send(prefix(name, "count"), String.valueOf(histogram.getCount())); send(prefix(name, "max"), String.valueOf(statistics.getMax())); send(prefix(name, "min"), String.valueOf(statistics.getMin())); send(prefix(name, "mean"), String.valueOf(statistics.getMean())); send(prefix(name, "stddev"), String.valueOf(statistics.getStdDev())); send(prefix(name, "p50"), String.valueOf(statistics.getQuantile(0.5))); send(prefix(name, "p75"), String.valueOf(statistics.getQuantile(0.75))); send(prefix(name, "p95"), String.valueOf(statistics.getQuantile(0.95))); send(prefix(name, "p98"), String.valueOf(statistics.getQuantile(0.98))); send(prefix(name, "p99"), String.valueOf(statistics.getQuantile(0.99))); send(prefix(name, "p999"), String.valueOf(statistics.getQuantile(0.999))); } } } private void reportMeter(final String name, final Meter meter) { if (meter != null) { send(prefix(name, "rate"), String.valueOf(meter.getRate())); send(prefix(name, "count"), String.valueOf(meter.getCount())); } } private String prefix(String ... names) { if (names.length > 0) { StringBuilder stringBuilder = new StringBuilder(names[0]); for (int i = 1; i < names.length; i++) { stringBuilder.append('.').append(names[i]); } return stringBuilder.toString(); } else { return ""; } } private void send(final String name, final String value) { try { String formatted = String.format("%s:%s|g", name, value); byte[] data = formatted.getBytes(StandardCharsets.UTF_8); socket.send(new DatagramPacket(data, data.length, this.address)); } catch (IOException e) { LOG.error("unable to send packet to statsd at '{}:{}'", address.getHostName(), address.getPort()); } } @Override public String filterCharacters(String input) { char[] chars = null; final int strLen = input.length(); int pos = 0; for (int i = 0; i < strLen; i++) { final char c = input.charAt(i); switch (c) { case ':': if (chars == null) { chars = input.toCharArray(); } chars[pos++] = '-'; break; default: if (chars != null) { chars[pos] = c; } pos++; } } return chars == null ? input : new String(chars, 0, pos); } }