package com.javamonitor;
import static com.javamonitor.JmxHelper.queryString;
import static com.javamonitor.mbeans.Server.httpPortAttribute;
import static com.javamonitor.mbeans.Server.nameAttribute;
import static com.javamonitor.mbeans.Server.serverObjectName;
import static java.lang.Integer.parseInt;
import static java.lang.System.getProperty;
import static java.net.Proxy.NO_PROXY;
import static java.net.Proxy.Type.HTTP;
import static java.util.logging.Logger.getLogger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import javax.management.ObjectName;
/**
* The data collector and interface to the collector server.
*
* @author Kees Jan Koster <kjkoster@kjkoster.org>
*/
final class Collector {
private static final Logger log = getLogger(Collector.class
.getName());
/**
* We locate and configure the use of an HTTP proxy, if configured. The
* standard proxy system properties seem to get ignored by our way of
* opening the connection, so we use the properties to explicitly create and
* use a proxy.
* <p>
* I considered introducing Java-monitor-specific settings, but I like this
* solution better. It makes the probe follow the normal way to configure
* proxies.
* <p>
* See also
* http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html.
*/
private final Proxy proxy;
private static final String JAVA_MONITOR_URL = "javamonitor.url";
private URL pushUrl = null;
private String account = null;
private final String uniqueId;
private String session = null;
private final Collection<Item> items = new LinkedList<Item>();
/**
* Create a new collector.
*
* @param url
* The URL to push to.
* @param uniqueId
* The unique ID to use instead of port number, or
* <code>null</code> to use the port number.
*/
Collector(final String uniqueId) {
this.uniqueId = uniqueId;
final String proxyHost = getProperty("http.proxyHost");
final int proxyPort = parseInt(getProperty("http.proxyPort", "80"));
final String proxyUser = getProperty("http.proxyUser");
final String proxyPass = getProperty("http.proxyPassword", "");
if (proxyHost != null) {
proxy = new Proxy(HTTP, new InetSocketAddress(proxyHost, proxyPort));
log.info("using proxy " + proxy);
if (proxyUser != null) {
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(proxyUser, proxyPass
.toCharArray());
}
});
}
} else {
proxy = NO_PROXY;
}
}
/**
* Sign in with the collector server and ask for permission to send
* statistics.
*
* @return <code>true</code> if the configuration was stale and we need to
* reconfigure. <code>false</code> if we may just reuse the existing
* list.
* @throws OnHoldException
* When we were put on hold by the server.
* @throws Exception
* When there was a problem.
*/
boolean push() throws Exception, OnHoldException {
init();
final Properties request = queryItems();
request.put("account", account);
request.put("localIp", getLocalIp());
if (uniqueId != null) {
request.put("lowestPort", uniqueId);
} else {
final String lowestPort = queryString(serverObjectName,
httpPortAttribute);
if (lowestPort != null) {
request.put("lowestPort", lowestPort);
}
}
request.put("appserver", queryString(serverObjectName, nameAttribute));
if (session != null) {
request.put(SESSION, session);
}
final Properties response = push(request);
return parse(response);
}
/**
* Retrieve the local IP address. If we just used InetAddress.getLocalhost()
* we end up with 127.0.0.1 in many cases, so instead we open a connection
* to the Java-monitor servers and use the local IP address of that
* connection.
* <p>
* We do not reuse the connection, because the JVM may be running on a
* laptop that moves from network to network.
*
* @return The local IP address of this JVM.
* @throws UnknownHostException
* When we could not determine the local IP address.
* @throws IOException
* When we could not determine the local IP address.
*/
private String getLocalIp() throws UnknownHostException, IOException {
Socket s = null;
try {
if (getProperty("http.proxyHost") != null) {
s = new Socket(getProperty("http.proxyHost"),
parseInt(getProperty("http.proxyPort", "80")));
} else {
int port = 80;
if (pushUrl.getPort() != -1) {
port = pushUrl.getPort();
}
s = new Socket(pushUrl.getHost(), port);
}
s.setSoTimeout(TWO_MINUTES);
return s.getLocalAddress().getHostAddress();
} finally {
if (s != null) {
try {
s.close();
} catch (IOException e) {
// ignore errors here...
}
}
}
}
private void init() throws Exception {
if (account == null) {
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(Collector.class
.getClassLoader().getResourceAsStream("uuid")));
account = in.readLine();
} finally {
if (in != null) {
in.close();
}
}
}
if (pushUrl == null) {
BufferedReader in = null;
try {
final String urlString;
if (System.getProperty(JAVA_MONITOR_URL) != null) {
urlString = getProperty(JAVA_MONITOR_URL);
} else {
in = new BufferedReader(new InputStreamReader(
Collector.class.getClassLoader()
.getResourceAsStream("pushUrl")));
urlString = in.readLine();
}
pushUrl = new URL(urlString);
} finally {
if (in != null) {
in.close();
}
}
}
}
/**
* Create a list of items to be pushed out to the server. As we go, we
* remove items that don't resolve to a value properly.
*
* @return The data to be sent to the server.
*/
private Properties queryItems() {
final Properties data = new Properties();
final Iterator<Item> itemIterator = items.iterator();
while (itemIterator.hasNext()) {
final Item item = itemIterator.next();
try {
int uniquefier = 0;
for (final ObjectName objectName : JmxHelper.queryNames(item
.getObjectName())) {
final String key = item.getId()
+ (uniquefier > 0 ? ":" + uniquefier : "");
final String actualObjectName = item.getObjectName()
.equals(objectName.toString()) ? "" : objectName
.toString();
final Object value = JmxHelper.query(objectName, item
.getAttribute());
if (value == null) {
data.put(key, "|0||" + actualObjectName);
} else {
data.put(key, value + "|" + getClassId(value) + "||"
+ actualObjectName);
}
uniquefier++;
}
// only push static items once per session
if (!item.isPeriodic()) {
itemIterator.remove();
}
} catch (Throwable e) {
final StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
data.put(item.getId(), "||" + e.getClass().getName() + ": "
+ sw.toString() + "|");
itemIterator.remove();
}
}
return data;
}
private static int getClassId(final Object value) {
if (value instanceof Byte || value instanceof Short
|| value instanceof Integer || value instanceof Long) {
return 1;
}
if (value instanceof Float || value instanceof Double) {
return 2;
}
if (value instanceof Boolean) {
return 3;
}
return 4; // String
}
private static final String ONHOLD = "onhold";
private static final String SESSION = "session";
private boolean parse(final Properties response) throws OnHoldException {
if (response.get(ONHOLD) != null) {
throw new OnHoldException((String) response.get(ONHOLD));
}
if (response.get(SESSION) != null) {
session = (String) response.remove(SESSION);
items.clear();
for (final Map.Entry<Object, Object> entry : response.entrySet()) {
final String[] parts = ((String) entry.getValue()).split("\\|");
items.add(new Item(entry.getKey().toString(), parts[0],
parts[1], Boolean.parseBoolean(parts[2])));
}
return true;
}
return false;
}
/**
* We set the timeout on the connection to be a few minutes. This way we are
* robust against network issues that cause probes to wait for data
* indefinitely.
*/
private static final int TWO_MINUTES = 2 * 60 * 1000;
private Properties push(final Properties request) throws IOException {
HttpURLConnection connection = null;
PrintStream out = null;
InputStream in = null;
try {
connection = (HttpURLConnection) pushUrl.openConnection(proxy);
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setConnectTimeout(TWO_MINUTES);
connection.setReadTimeout(TWO_MINUTES);
connection.setRequestProperty("Connection", "close");
out = new PrintStream(connection.getOutputStream());
request.storeToXML(out, null);
out.flush();
in = connection.getInputStream();
final Properties response = new Properties();
response.loadFromXML(in);
return response;
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
// ignore...
}
}
if (out != null) {
try {
out.close();
} catch (Exception e) {
// ignore...
}
}
if (connection != null) {
try {
connection.disconnect();
} catch (Exception e) {
// ignore...
}
}
}
}
}