From ce947870ed120589c49b2e080ea7be17f8ef07b0 Mon Sep 17 00:00:00 2001 From: evernat Date: Tue, 23 May 2017 02:15:59 +0200 Subject: [PATCH] publish data in Graphite, if Graphite address configured --- .../java/net/bull/javamelody/Collector.java | 100 +++++++------ .../java/net/bull/javamelody/Graphite.java | 132 ++++++++++++++++++ .../java/net/bull/javamelody/Parameter.java | 7 +- 3 files changed, 196 insertions(+), 43 deletions(-) create mode 100644 javamelody-core/src/main/java/net/bull/javamelody/Graphite.java diff --git a/javamelody-core/src/main/java/net/bull/javamelody/Collector.java b/javamelody-core/src/main/java/net/bull/javamelody/Collector.java index fe60edf6d..80d6a2d8a 100644 --- a/javamelody-core/src/main/java/net/bull/javamelody/Collector.java +++ b/javamelody-core/src/main/java/net/bull/javamelody/Collector.java @@ -83,6 +83,7 @@ class Collector { // NOPMD private Date lastDateOfDeletedObsoleteFiles = new Date(); private boolean stopped; private final boolean noDatabase = Parameters.isNoDatabase(); + private final Graphite graphite = Graphite.getInstance(); /** * Les versions de l'applications avec pour chacune la date de déploiement. */ @@ -361,23 +362,29 @@ void collectWithoutErrors(List javaInformationsList) { private synchronized long collect(List javaInformationsList) throws IOException { - // si pas d'informations, on ne met pas 0 : on ne met rien - if (!javaInformationsList.isEmpty()) { - collectJavaInformations(javaInformationsList); - collectOtherJavaInformations(javaInformationsList); - collectTomcatInformations(javaInformationsList); - } long memorySize = 0; - for (final Counter counter : counters) { - // counter.isDisplayed() peut changer pour spring, ejb, guice ou services selon l'utilisation - dayCountersByCounter.get(counter).setDisplayed(counter.isDisplayed()); - // collecte pour chaque compteur (hits par minute, temps moyen, % d'erreurs système) - // Rq : il serait possible d'ajouter le débit total en Ko / minute (pour http) - // mais autant monitorer les vrais débits réseaux au niveau de l'OS - if (counter.isDisplayed()) { - // si le compteur n'est pas affiché (par ex ejb), pas de collecte - // et pas de persistance de fichiers jrobin ou du compteur - memorySize += collectCounterData(counter); + try { + // si pas d'informations, on ne met pas 0 : on ne met rien + if (!javaInformationsList.isEmpty()) { + collectJavaInformations(javaInformationsList); + collectOtherJavaInformations(javaInformationsList); + collectTomcatInformations(javaInformationsList); + } + for (final Counter counter : counters) { + // counter.isDisplayed() peut changer pour spring, ejb, guice ou services selon l'utilisation + dayCountersByCounter.get(counter).setDisplayed(counter.isDisplayed()); + // collecte pour chaque compteur (hits par minute, temps moyen, % d'erreurs système) + // Rq : il serait possible d'ajouter le débit total en Ko / minute (pour http) + // mais autant monitorer les vrais débits réseaux au niveau de l'OS + if (counter.isDisplayed()) { + // si le compteur n'est pas affiché (par ex ejb), pas de collecte + // et pas de persistance de fichiers jrobin ou du compteur + memorySize += collectCounterData(counter); + } + } + } finally { + if (graphite != null) { + graphite.send(); } } @@ -496,9 +503,9 @@ private void collectOtherJavaInformations(List javaInformation final int gcPercentage = Math .min((int) ((garbageCollectionTimeMillis - this.gcTimeMillis) * 100 / periodMillis / availableProcessors), 100); - getOtherJRobin("gc").addValue(gcPercentage); + addJRobinValue(getOtherJRobin("gc"), gcPercentage); } else { - getOtherJRobin("gc").addValue(0d); + addJRobinValue(getOtherJRobin("gc"), 0d); } this.gcTimeMillis = garbageCollectionTimeMillis; } @@ -515,7 +522,7 @@ private void collectOtherJavaInformations(List javaInformation otherJRobinsValues.put("fileDescriptors", (double) unixOpenFileDescriptorCount); for (final Map.Entry entry : otherJRobinsValues.entrySet()) { if (entry.getValue() >= 0) { - getOtherJRobin(entry.getKey()).addValue(entry.getValue()); + addJRobinValue(getOtherJRobin(entry.getKey()), entry.getValue()); } } @@ -525,16 +532,16 @@ private void collectOtherJavaInformations(List javaInformation // collecte du nombre de transactions base de données par minute if (this.transactionCount != NOT_A_NUMBER) { final double periodMinutes = periodMillis / 60000d; - getOtherJRobin("transactionsRate").addValue( + addJRobinValue(getOtherJRobin("transactionsRate"), (databaseTransactionCount - this.transactionCount) / periodMinutes); } else { - getOtherJRobin("transactionsRate").addValue(0d); + addJRobinValue(getOtherJRobin("transactionsRate"), 0d); } this.transactionCount = databaseTransactionCount; } if (freeDiskSpaceInTemp != Long.MAX_VALUE) { - getOtherJRobin("Free_disk_space").addValue(freeDiskSpaceInTemp); + addJRobinValue(getOtherJRobin("Free_disk_space"), freeDiskSpaceInTemp); } // on pourrait collecter la valeur 100 dans jrobin pour qu'il fasse la moyenne @@ -570,7 +577,7 @@ private void collectJRobinValues(long usedMemory, long processesCpuTimeMillis, int availableProcessors, int sessionCount, int activeThreadCount, int activeConnectionCount, int usedConnectionCount) throws IOException { // collecte de la mémoire java - getCounterJRobin("usedMemory").addValue(usedMemory); + addJRobinValue(getCounterJRobin("usedMemory"), usedMemory); // collecte du pourcentage d'utilisation cpu if (processesCpuTimeMillis >= 0) { @@ -587,9 +594,9 @@ private void collectJRobinValues(long usedMemory, long processesCpuTimeMillis, final int cpuPercentage = Math .min((int) ((processesCpuTimeMillis - this.cpuTimeMillis) * 100 / periodMillis / availableProcessors), 100); - getCounterJRobin("cpu").addValue(cpuPercentage); + addJRobinValue(getCounterJRobin("cpu"), cpuPercentage); } else { - getCounterJRobin("cpu").addValue(0d); + addJRobinValue(getCounterJRobin("cpu"), 0d); } this.cpuTimeMillis = processesCpuTimeMillis; } @@ -599,37 +606,37 @@ private void collectJRobinValues(long usedMemory, long processesCpuTimeMillis, if (getCounterByName(Counter.HTTP_COUNTER_NAME) != null) { // collecte du nombre de sessions http if (sessionCount >= 0) { - getCounterJRobin("httpSessions").addValue(sessionCount); + addJRobinValue(getCounterJRobin("httpSessions"), sessionCount); } // collecte du nombre de threads actifs (requêtes http en cours) - getCounterJRobin("activeThreads").addValue(activeThreadCount); + addJRobinValue(getCounterJRobin("activeThreads"), activeThreadCount); } if (!noDatabase) { // collecte du nombre de connexions jdbc actives et du nombre de connexions jdbc ouvertes - getCounterJRobin("activeConnections").addValue(activeConnectionCount); - getCounterJRobin("usedConnections").addValue(usedConnectionCount); + addJRobinValue(getCounterJRobin("activeConnections"), activeConnectionCount); + addJRobinValue(getCounterJRobin("usedConnections"), usedConnectionCount); } // si ce collector est celui des nodes Jenkins, on collecte le nombre de builds en cours // pour le graphique if (getCounterByName(Counter.BUILDS_COUNTER_NAME) != null) { - getCounterJRobin("runningBuilds").addValue(JdbcWrapper.getRunningBuildCount()); - getCounterJRobin("buildQueueLength").addValue(JdbcWrapper.getBuildQueueLength()); + addJRobinValue(getCounterJRobin("runningBuilds"), JdbcWrapper.getRunningBuildCount()); + addJRobinValue(getCounterJRobin("buildQueueLength"), JdbcWrapper.getBuildQueueLength()); } } private void collectTomcatValues(int tomcatBusyThreads, long bytesReceived, long bytesSent) throws IOException { - getOtherJRobin("tomcatBusyThreads").addValue(tomcatBusyThreads); + addJRobinValue(getOtherJRobin("tomcatBusyThreads"), tomcatBusyThreads); if (this.tomcatBytesSent != NOT_A_NUMBER) { final double periodMinutes = periodMillis / 60000d; - getOtherJRobin("tomcatBytesReceived") - .addValue((bytesReceived - this.tomcatBytesReceived) / periodMinutes); - getOtherJRobin("tomcatBytesSent") - .addValue((bytesSent - this.tomcatBytesSent) / periodMinutes); + addJRobinValue(getOtherJRobin("tomcatBytesReceived"), + (bytesReceived - this.tomcatBytesReceived) / periodMinutes); + addJRobinValue(getOtherJRobin("tomcatBytesSent"), + (bytesSent - this.tomcatBytesSent) / periodMinutes); } else { - getOtherJRobin("tomcatBytesReceived").addValue(0d); - getOtherJRobin("tomcatBytesSent").addValue(0d); + addJRobinValue(getOtherJRobin("tomcatBytesReceived"), 0d); + addJRobinValue(getOtherJRobin("tomcatBytesSent"), 0d); } this.tomcatBytesReceived = bytesReceived; this.tomcatBytesSent = bytesSent; @@ -643,7 +650,14 @@ private void collectSessionsMeanAge(long sessionAgeSum, int sessionCount) throws } else { sessionAgeMeanInMinutes = -1; } - getOtherJRobin("httpSessionsMeanAge").addValue(sessionAgeMeanInMinutes); + addJRobinValue(getOtherJRobin("httpSessionsMeanAge"), sessionAgeMeanInMinutes); + } + } + + private void addJRobinValue(JRobin jRobin, double value) throws IOException { + jRobin.addValue(value); + if (graphite != null) { + graphite.addValue(jRobin.getName(), value); } } @@ -725,11 +739,12 @@ private long collectCounterData(Counter counter) throws IOException { final long hitsParMinute = hits * 60 * 1000 / periodMillis; // on remplit le stockage avec les données - hitsJRobin.addValue(hitsParMinute); + addJRobinValue(hitsJRobin, hitsParMinute); // s'il n'y a pas eu de hits, alors la moyenne vaut -1 : elle n'a pas de sens if (hits > 0) { - meanTimesJRobin.addValue(lastPeriodGlobalRequest.getMean()); - systemErrorsJRobin.addValue(lastPeriodGlobalRequest.getSystemErrorPercentage()); + addJRobinValue(meanTimesJRobin, lastPeriodGlobalRequest.getMean()); + addJRobinValue(systemErrorsJRobin, + lastPeriodGlobalRequest.getSystemErrorPercentage()); // s'il y a eu des requêtes, on persiste le compteur pour ne pas perdre les stats // en cas de crash ou d'arrêt brutal (mais normalement ils seront aussi persistés @@ -824,6 +839,7 @@ private void collectCounterRequestData(Counter dayCounter, CounterRequest newReq newRequest.getName()); // plus nécessaire: if (dayCounter.isErrorCounter()) requestJRobin.addValue(lastPeriodRequest.getHits()); + // pas addJRobinValue ici, il y en aurait trop pour Graphite requestJRobin.addValue(lastPeriodRequest.getMean()); } diff --git a/javamelody-core/src/main/java/net/bull/javamelody/Graphite.java b/javamelody-core/src/main/java/net/bull/javamelody/Graphite.java new file mode 100644 index 000000000..50a0dc07b --- /dev/null +++ b/javamelody-core/src/main/java/net/bull/javamelody/Graphite.java @@ -0,0 +1,132 @@ +/* + * Copyright 2008-2017 by Emeric Vernat + * + * This file is part of Java Melody. + * + * 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 + * + * 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 net.bull.javamelody; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +import javax.net.SocketFactory; + +/** + * Publish chart data to Graphite, http://graphiteapp.org/. + * @author Emeric Vernat + */ +class Graphite { + private static final int DEFAULT_GRAPHITE_PORT = 2003; + private static final char SEPARATOR = ' '; + + private final SocketFactory socketFactory; + private final InetAddress address; + private final int port; + private final String prefix; + + private final DecimalFormat decimalFormat = new DecimalFormat("0.00", + DecimalFormatSymbols.getInstance(Locale.US)); + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final Writer bufferWriter; + private long lastTime; + private String lastTimestamp; + + Graphite(SocketFactory socketFactory, InetAddress address, int port, Charset charset, + String prefix) { + super(); + assert socketFactory != null; + assert address != null; + assert charset != null; + assert prefix != null; + this.socketFactory = socketFactory; + this.address = address; + this.port = port; + this.prefix = prefix; + this.bufferWriter = new OutputStreamWriter(buffer, charset); + } + + static Graphite getInstance() { + final String graphiteAddress = Parameters.getParameter(Parameter.GRAPHITE_ADDRESS); + if (graphiteAddress != null) { + final String address; + final int port; + final int index = graphiteAddress.indexOf(':'); + if (index != -1 && index < graphiteAddress.length()) { + address = graphiteAddress.substring(0, index); + port = Integer + .parseInt(graphiteAddress.substring(index + 1, graphiteAddress.length())); + } else { + address = graphiteAddress; + port = DEFAULT_GRAPHITE_PORT; + } + try { + return new Graphite(SocketFactory.getDefault(), InetAddress.getByName(address), + port, Charset.forName("UTF-8"), getDefaultPrefix()); + } catch (final UnknownHostException e) { + throw new IllegalArgumentException("Invalid host: " + address, e); + } + } + return null; + } + + synchronized void addValue(String metric, double value) throws IOException { + final long timeInSeconds = System.currentTimeMillis() / 1000; + if (lastTime != timeInSeconds) { + lastTimestamp = String.valueOf(timeInSeconds); + lastTime = timeInSeconds; + } + bufferWriter.append(prefix).append('.').append(metric).append(' '); + bufferWriter.append(decimalFormat.format(value)).append(' '); + bufferWriter.append(lastTimestamp).append('\n'); + } + + synchronized void send() throws IOException { + try { + bufferWriter.flush(); + final Socket socket = createSocket(); + try { + buffer.writeTo(socket.getOutputStream()); + } finally { + socket.close(); + } + } catch (final ConnectException e) { + throw new IOException("Error connecting to Graphite at " + address + ':' + port, e); + } finally { + // finally to be sure to not keep too much data in buffer + // including when the socket can't connect + buffer.reset(); + } + } + + private Socket createSocket() throws IOException { + return socketFactory.createSocket(address, port); + } + + private static String getDefaultPrefix() { + final String defaultPrefix = "javamelody." + Parameters + .getContextPath(Parameters.getServletContext()).replace("/", "").replace('.', '_') + + '.' + Parameters.getHostName().replace('.', '_'); + return defaultPrefix.replace(SEPARATOR, '_'); + } +} diff --git a/javamelody-core/src/main/java/net/bull/javamelody/Parameter.java b/javamelody-core/src/main/java/net/bull/javamelody/Parameter.java index 5da906c88..06f40c5c1 100644 --- a/javamelody-core/src/main/java/net/bull/javamelody/Parameter.java +++ b/javamelody-core/src/main/java/net/bull/javamelody/Parameter.java @@ -338,7 +338,12 @@ public enum Parameter { /** * If Real User Monitoring enabled by injecting Boomerang javascript into html page ("false" by default). */ - RUM_ENABLED("rum-enabled"); + RUM_ENABLED("rum-enabled"), + + /** + * Address of the Graphite (http://graphiteapp.org/) server to send metrics to, for example: 11.22.33.44:2003 (null by default). + */ + GRAPHITE_ADDRESS("graphite-address"); private final String code;