net.bull.javamelody.internal.publish.Graphite Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of javamelody-core Show documentation
Show all versions of javamelody-core Show documentation
Monitoring of JavaEE applications
/*
* Copyright 2008-2019 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.internal.publish;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import javax.net.SocketFactory;
import net.bull.javamelody.Parameter;
import net.bull.javamelody.internal.common.LOG;
/**
* Publish chart data to Graphite.
* @author Emeric Vernat
*/
class Graphite extends MetricsPublisher {
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(String contextPath, String hostName) {
final String graphiteAddress = Parameter.GRAPHITE_ADDRESS.getValue();
if (graphiteAddress != null) {
assert contextPath != null;
assert hostName != null;
final String address;
final int port;
final int index = graphiteAddress.lastIndexOf(':');
if (index != -1) {
// we could use new URI("socket://" + graphiteAddress).getHost() and getPort() to parse the address
address = graphiteAddress.substring(0, index);
port = Integer.parseInt(graphiteAddress.substring(index + 1));
} else {
address = graphiteAddress;
port = DEFAULT_GRAPHITE_PORT;
}
// contextPath est du genre "/testapp"
// hostName est du genre "www.host.com"
final String prefix = ("javamelody." + contextPath.replace("/", "") + '.'
+ hostName.replace('.', '_') + '.').replace(SEPARATOR, '_');
try {
return new Graphite(SocketFactory.getDefault(), InetAddress.getByName(address),
port, StandardCharsets.UTF_8, prefix);
} catch (final UnknownHostException e) {
throw new IllegalArgumentException("Invalid host: " + address, e);
}
}
return null;
}
@Override
public 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(metric).append(SEPARATOR);
bufferWriter.append(decimalFormat.format(value)).append(SEPARATOR);
bufferWriter.append(lastTimestamp).append('\n');
}
@Override
public synchronized void send() throws IOException {
try {
bufferWriter.flush();
try (Socket socket = createSocket()) {
buffer.writeTo(socket.getOutputStream());
checkNoReturnedData(socket);
}
} 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);
}
/**
* 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.
* @param socket Socket
* @throws IOException e
*/
private void checkNoReturnedData(Socket socket) throws IOException {
final InputStream input = socket.getInputStream();
if (input.available() > 0) {
final byte[] bytes = new byte[1000];
final int toRead = Math.min(input.available(), bytes.length);
final int read = input.read(bytes, 0, toRead);
if (read > 0) {
final String msg = "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 " + read
+ " bytes of response: "
+ new String(bytes, 0, read, StandardCharsets.UTF_8);
LOG.warn(msg, new IOException(msg));
}
}
}
@Override
public void stop() {
// nothing
}
}