/**
* The MIT License
* Copyright © 2010 JmxTrans team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.googlecode.jmxtrans.model.output;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Base64Variants;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.googlecode.jmxtrans.model.Query;
import com.googlecode.jmxtrans.model.Result;
import com.googlecode.jmxtrans.model.Server;
import com.googlecode.jmxtrans.model.ValidationException;
import com.googlecode.jmxtrans.model.naming.KeyUtils;
import com.googlecode.jmxtrans.model.naming.StringUtils;
import lombok.EqualsAndHashCode;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.googlecode.jmxtrans.util.NumberUtils.isNumeric;
/**
* This writer is a port of the LibratoWriter from the embedded-jmxtrans
* project.
* <p/>
* <a href="https://metrics.librato.com//">Librato Metrics</a>
* <p/>
* This implementation uses <a href="http://dev.librato.com/v1/post/metrics">
* POST {@code /v1/metrics}</a> HTTP API.
* <p/>
* Settings:
* <ul>
* <li>"{@code url}": Librato server URL. Optional, default value:
* {@value #DEFAULT_LIBRATO_API_URL}.</li>
* <li>"{@code username}": Librato username. Mandatory</li>
* <li>"{@code token}": Librato token. Mandatory</li>
* <li>"{@code libratoApiTimeoutInMillis}": read timeout of the calls to Librato
* HTTP API. Optional, default value: 1000.</li>
* </ul>
*
* @author <a href="mailto:cleclerc@cloudbees.com">Cyrille Le Clerc</a>
*/
@EqualsAndHashCode(exclude = {"jsonFactory"})
public class LibratoWriter extends BaseOutputWriter {
public static final String SETTING_URL = "url";
public static final String SETTING_USERNAME = "username";
public static final String SETTING_TOKEN = "token";
public static final String SETTING_PROXY_HOST = "proxyHost";
public static final String SETTING_PROXY_PORT = "proxyPort";
public static final String DEFAULT_LIBRATO_API_URL = "https://metrics-api.librato.com/v1/metrics";
public static final String SETTING_LIBRATO_API_TIMEOUT_IN_MILLIS = "libratoApiTimeoutInMillis";
private static final Logger logger = LoggerFactory.getLogger(LibratoWriter.class);
private final JsonFactory jsonFactory = new JsonFactory();
/**
* Librato HTTP API URL
*/
private final URL url;
private final int libratoApiTimeoutInMillis;
/**
* Librato HTTP API authentication username
*/
private final String username;
private final String token;
private final String basicAuthentication;
/**
* Optional proxy for the http API calls
*/
private final String proxyHost;
private final Integer proxyPort;
private Proxy proxy;
@VisibleForTesting
final String httpUserAgent;
@JsonCreator
public LibratoWriter(
@JsonProperty("typeNames") ImmutableList<String> typeNames,
@JsonProperty("booleanAsNumber") boolean booleanAsNumber,
@JsonProperty("debug") Boolean debugEnabled,
@JsonProperty("url") URL url,
@JsonProperty("libratoApiTimeoutInMillis") Integer libratoApiTimeoutInMillis,
@JsonProperty("username") String username,
@JsonProperty("token") String token,
@JsonProperty("proxyHost") String proxyHost,
@JsonProperty("proxyPort") Integer proxyPort,
@JsonProperty("settings") Map<String, Object> settings) throws MalformedURLException {
super(typeNames, booleanAsNumber, debugEnabled, settings);
logger.warn("LibratoWriter is deprecated. Please use LibratoWriterFactory instead.");
this.url = MoreObjects.firstNonNull(
url,
new URL(MoreObjects.firstNonNull(
(String) this.getSettings().get(SETTING_URL),
DEFAULT_LIBRATO_API_URL)));
this.libratoApiTimeoutInMillis = MoreObjects.firstNonNull(
libratoApiTimeoutInMillis,
Settings.getIntSetting(getSettings(), SETTING_LIBRATO_API_TIMEOUT_IN_MILLIS, 1000));
this.username = MoreObjects.firstNonNull(
username,
(String) getSettings().get(SETTING_USERNAME));
this.token = MoreObjects.firstNonNull(
token,
(String) getSettings().get(SETTING_TOKEN));
this.basicAuthentication = Base64Variants
.getDefaultVariant()
.encode((this.username + ":" + this.token).getBytes(Charsets.US_ASCII));
this.proxyHost = proxyHost != null ? proxyHost : (String) getSettings().get(SETTING_PROXY_HOST);
this.proxyPort = proxyPort != null ? proxyPort : Settings.getIntegerSetting(getSettings(), SETTING_PROXY_PORT, null);
if (this.proxyHost != null && this.proxyPort != null) {
this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.proxyHost, this.proxyPort));
} else {
this.proxy = null;
}
this.httpUserAgent =
"jmxtrans-standalone/1 " + "(" +
System.getProperty("java.vm.name") + "/" + System.getProperty("java.version") + "; " +
System.getProperty("os.name") + "-" + System.getProperty("os.arch") + "/" + System.getProperty("os.version")
+ ")";
}
@Override
public void validateSetup(Server server, Query query) throws ValidationException {
logger.info("Start Librato writer connected to '{}', proxy {} with username '{}' ...", url, proxy, username);
}
@Override
public void internalWrite(Server server, Query query, ImmutableList<Result> results) throws Exception {
logger.debug("Export to '{}', proxy {} metrics {}", url, proxy, query);
writeToLibrato(server, query, results);
}
private void serialize(Server server, Query query, List<Result> results, OutputStream outputStream) throws IOException {
JsonGenerator g = jsonFactory.createGenerator(outputStream, JsonEncoding.UTF8);
g.writeStartObject();
g.writeArrayFieldStart("counters");
g.writeEndArray();
String source = getSource(server);
g.writeArrayFieldStart("gauges");
List<String> typeNames = getTypeNames();
for (Result result : results) {
Map<String, Object> resultValues = result.getValues();
for (Map.Entry<String, Object> values : resultValues.entrySet()) {
if (isNumeric(values.getValue())) {
g.writeStartObject();
g.writeStringField("name", KeyUtils.getKeyString(query, result, values, typeNames));
if (source != null && !source.isEmpty()) {
g.writeStringField("source", source);
}
g.writeNumberField("measure_time", TimeUnit.SECONDS.convert(result.getEpoch(), TimeUnit.MILLISECONDS));
Object value = values.getValue();
if (value instanceof Integer) {
g.writeNumberField("value", (Integer) value);
} else if (value instanceof Long) {
g.writeNumberField("value", (Long) value);
} else if (value instanceof Float) {
g.writeNumberField("value", (Float) value);
} else if (value instanceof Double) {
g.writeNumberField("value", (Double) value);
}
g.writeEndObject();
}
}
}
g.writeEndArray();
g.writeEndObject();
g.flush();
g.close();
}
private void writeToLibrato(Server server, Query query, List<Result> results) {
HttpURLConnection urlConnection = null;
try {
if (proxy == null) {
urlConnection = (HttpURLConnection) url.openConnection();
} else {
urlConnection = (HttpURLConnection) url.openConnection(proxy);
}
urlConnection.setRequestMethod("POST");
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
urlConnection.setReadTimeout(libratoApiTimeoutInMillis);
urlConnection.setRequestProperty("content-type", "application/json; charset=utf-8");
urlConnection.setRequestProperty("Authorization", "Basic " + basicAuthentication);
urlConnection.setRequestProperty("User-Agent", httpUserAgent);
serialize(server, query, results, urlConnection.getOutputStream());
int responseCode = urlConnection.getResponseCode();
if (responseCode != 200) {
logger.warn("Failure {}:'{}' to send result to Librato server '{}' with proxy {}, username {}", responseCode, urlConnection.getResponseMessage(), url, proxy, username);
}
if (logger.isTraceEnabled()) {
StringWriter out = new StringWriter();
IOUtils.copy(urlConnection.getInputStream(), out);
logger.trace(out.toString());
}
} catch (Exception e) {
logger.warn("Failure to send result to Librato server '{}' with proxy {}, username {}", url, proxy, username, e);
} finally {
if (urlConnection != null) {
try {
InputStream in = urlConnection.getInputStream();
IOUtils.copy(in, NullOutputStream.NULL_OUTPUT_STREAM);
IOUtils.closeQuietly(in);
InputStream err = urlConnection.getErrorStream();
if (err != null) {
IOUtils.copy(err, NullOutputStream.NULL_OUTPUT_STREAM);
IOUtils.closeQuietly(err);
}
} catch (IOException e) {
logger.warn("Exception flushing http connection", e);
}
}
}
}
private String getSource(Server server) {
if (server.getAlias() != null) {
return server.getAlias();
} else {
return StringUtils.cleanupStr(server.getHost());
}
}
public URL getUrl() {
return url;
}
public int getLibratoApiTimeoutInMillis() {
return libratoApiTimeoutInMillis;
}
public String getUsername() {
return username;
}
public String getToken() {
return token;
}
public String getProxyHost() {
return proxyHost;
}
public Integer getProxyPort() {
return proxyPort;
}
}