/** * Copyright 2013 Netflix, Inc. * <p/> * Licensed 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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 com.netflix.servo.publish.graphite; import com.netflix.servo.Metric; import com.netflix.servo.publish.BaseMetricObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.SocketFactory; import java.io.BufferedInputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.util.List; /** * Observer that shunts metrics out to the excellent monitoring backend graphite. * <p/> * Developed and tested against version 0.9.10 of graphite. * <p/> * http://graphite.wikidot.com/ */ public class GraphiteMetricObserver extends BaseMetricObserver { private static final Logger LOGGER = LoggerFactory.getLogger(GraphiteMetricObserver.class); private final GraphiteNamingConvention namingConvention; private final String serverPrefix; private final SocketFactory socketFactory = SocketFactory.getDefault(); private final URI graphiteServerURI; private Socket socket = null; /** * Creates a new instance. * * @param metricPrefix base name to attach onto each metric published * ("metricPrefix.{rest of name}". If null * this section isn't attached. This section is useful to * differentiate between multiple * nodes of the same application server in a cluster. * @param graphiteServerAddress address of the graphite data port in "host:port" format. */ public GraphiteMetricObserver(String metricPrefix, String graphiteServerAddress) { this(metricPrefix, graphiteServerAddress, new BasicGraphiteNamingConvention()); } /** * Creates a new instance. * * @param metricPrefix base name to attach onto each metric published * ("metricPrefix.{rest of name}". If null * this section isn't attached. This section is useful to * differentiate between multiple * nodes of the same application server in a cluster. * @param graphiteServerAddress address of the graphite data port in "host:port" format. * @param namingConvention naming convention to extract a graphite compatible name from * each Metric */ public GraphiteMetricObserver(String metricPrefix, String graphiteServerAddress, GraphiteNamingConvention namingConvention) { super("GraphiteMetricObserver" + metricPrefix); this.namingConvention = namingConvention; this.serverPrefix = metricPrefix; this.graphiteServerURI = parseStringAsUri(graphiteServerAddress); } /** * Stop sending metrics to the graphite server. */ public void stop() { try { if (socket != null) { socket.close(); socket = null; LOGGER.info("Disconnected from graphite server: {}", graphiteServerURI); } } catch (IOException e) { LOGGER.warn("Error Stopping", e); } } @Override public void updateImpl(List<Metric> metrics) { try { if (connectionAvailable()) { write(socket, metrics); } } catch (IOException e) { LOGGER.warn("Graphite connection failed on write", e); incrementFailedCount(); // disconnect so next time when connectionAvailable is called it can try to reconnect stop(); } } private boolean connectionAvailable() throws IOException { if (socket == null || !socket.isConnected()) { if (socket != null) { socket.close(); } socket = socketFactory.createSocket(graphiteServerURI.getHost(), graphiteServerURI.getPort()); LOGGER.info("Connected to graphite server: {}", graphiteServerURI); } return socket.isConnected(); } private void write(Socket socket, Iterable<Metric> metrics) throws IOException { PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); int count = writeMetrics(metrics, writer); boolean checkError = writer.checkError(); if (checkError) { throw new IOException("Writing to socket has failed"); } checkNoReturnedData(socket); LOGGER.debug("Wrote {} metrics to graphite", count); } private int writeMetrics(Iterable<Metric> metrics, PrintWriter writer) { int count = 0; for (Metric metric : metrics) { String publishedName = namingConvention.getName(metric); StringBuilder sb = new StringBuilder(); if (serverPrefix != null) { sb.append(serverPrefix).append("."); } sb.append(publishedName).append(" ") .append(metric.getValue().toString()) .append(" ") .append(metric.getTimestamp() / 1000); LOGGER.debug("{}", sb); writer.write(sb.append("\n").toString()); count++; } return count; } /** * the graphite protocol is a "one-way" streaming protocol and as such there is no easy way * to check that we are sending the output to the wrong place. We can aim the graphite writer * at any listening socket and it will never care if the data is being correctly handled. By * logging any returned bytes we can help make it obvious that it's talking to the wrong * server/socket. In particular if its aimed at the web port of a * graphite server this will make it print out the HTTP error message. */ private void checkNoReturnedData(Socket socket) throws IOException { BufferedInputStream reader = new BufferedInputStream(socket.getInputStream()); if (reader.available() > 0) { byte[] buffer = new byte[1000]; int toRead = Math.min(reader.available(), 1000); int read = reader.read(buffer, 0, toRead); if (read > 0) { LOGGER.warn("Data returned by graphite server when expecting no response! " + "Probably aimed at wrong socket or server. Make sure you " + "are publishing to the data port, not the dashboard port. " + "First {} bytes of response: {}", read, new String(buffer, 0, read, "UTF-8")); } } } /** * It's a lot easier to configure and manage the location of the graphite server if we combine * the ip and port into a single string. Using a "fake" transport and the ipString means we get * standard host/port parsing (including domain names, ipv4 and ipv6) for free. */ private static URI parseStringAsUri(String ipString) { try { URI uri = new URI("socket://" + ipString); if (uri.getHost() == null || uri.getPort() == -1) { throw new URISyntaxException(ipString, "URI must have host and port parts"); } return uri; } catch (URISyntaxException e) { throw (IllegalArgumentException) new IllegalArgumentException( "Graphite server address needs to be defined as {host}:{port}.").initCause(e); } } }