package se.l4.vibe.influxdb;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import se.l4.vibe.backend.VibeBackend;
import se.l4.vibe.event.EventListener;
import se.l4.vibe.event.EventSeverity;
import se.l4.vibe.event.Events;
import se.l4.vibe.influxdb.internal.DataPoint;
import se.l4.vibe.influxdb.internal.DataQueue;
import se.l4.vibe.mapping.KeyValueMappable;
import se.l4.vibe.mapping.KeyValueReceiver;
import se.l4.vibe.probes.Probe;
import se.l4.vibe.probes.SampleListener;
import se.l4.vibe.probes.SampledProbe;
import se.l4.vibe.probes.Sampler;
import se.l4.vibe.timer.Timer;
import se.l4.vibe.timer.TimerListener;
/**
* {@link VibeBackend Backend} that sends data to InfluxDB.
*
* @author Andreas Holstenson
*
*/
public class InfluxDBBackend
implements VibeBackend
{
private static final Logger logger = LoggerFactory.getLogger(InfluxDBBackend.class);
private static final MediaType MEDIA_TYPE = MediaType.parse("text/plain");
private final String url;
private final String auth;
private final Map<String, String> tags;
private final OkHttpClient client;
private final DataQueue queue;
private final ScheduledExecutorService executor;
private InfluxDBBackend(String url, String username, String password, String db, Map<String, String> tags)
{
this.tags = tags;
client = new OkHttpClient();
this.url = HttpUrl.parse(url).newBuilder()
.addPathSegment("write")
.addQueryParameter("db", db)
.addQueryParameter("precision", "ms")
.build()
.toString();
if(username != null)
{
auth = "Basic " + Base64.getMimeEncoder().encodeToString((username + ':' + password).getBytes(StandardCharsets.UTF_8));
}
else
{
auth = null;
}
executor = Executors.newScheduledThreadPool(1, new ThreadFactory()
{
@Override
public Thread newThread(Runnable r)
{
Thread thread = new Thread(r);
thread.setName("InfluxDB[" + url + "]");
return thread;
}
});
queue = new DataQueue(this::send, executor);
}
private void send(String data)
{
Request.Builder builder = new Request.Builder()
.url(url)
.post(RequestBody.create(MEDIA_TYPE, data));
if(auth != null)
{
builder.addHeader("Authorization", auth);
}
Request request = builder.build();
try
{
Response response = client.newCall(request)
.execute();
response.body().close();
if(response.code() < 200 || response.code() >= 300)
{
logger.warn("Unable to store values; Got response code " + response.code());
throw new RuntimeException("Failed sending");
}
}
catch(IOException e)
{
logger.warn("Unable to store values; " + e.getMessage(), e);
throw new RuntimeException("Failed sending; " + e.getMessage(), e);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void export(String path, Sampler<?> sampler)
{
((Sampler) sampler).addListener(new SampleQueuer(path));
}
@Override
public void export(String path, Probe<?> probe)
{
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void export(String path, Events<?> events)
{
((Events) events).addListener(new EventQueuer(path));
}
@Override
public void export(String path, Timer timer)
{
timer.addListener(new TimerQueuer(path));
}
@Override
public void close()
{
queue.close();
executor.shutdown();
}
private class SampleQueuer
implements SampleListener<Object>
{
private final String path;
public SampleQueuer(String path)
{
this.path = path;
}
@Override
public void sampleAcquired(SampledProbe<Object> probe, Sampler.Entry<Object> entry)
{
Object value = entry.getValue();
Map<String, Object> values = new HashMap<>();
KeyValueReceiver receiver = (key, v) -> {
if((v instanceof Double && Double.isNaN((Double) v))
|| (v instanceof Float && Float.isNaN((Float) v)))
{
// Skip NaN values
return;
}
values.put(key, v);
};
if(value instanceof KeyValueMappable)
{
((KeyValueMappable) value).mapToKeyValues(receiver);
}
else
{
receiver.add("value", value);
}
// TODO: Can a probe provide extra tags?
DataPoint point = new DataPoint(path, entry.getTime(), tags, values);
queue.add(point);
}
}
private class TimerQueuer
implements TimerListener
{
private final String path;
public TimerQueuer(String path)
{
this.path = path;
}
@Override
public void timerEvent(long now, long timeInNanoseconds)
{
HashMap<String, Object> map = new HashMap<>();
map.put("value", timeInNanoseconds);
DataPoint point = new DataPoint(path, now, tags, map);
queue.add(point);
}
}
private class EventQueuer
implements EventListener<Object>
{
private final String path;
public EventQueuer(String path)
{
this.path = path;
}
@Override
public void eventRegistered(Events<Object> events, EventSeverity severity, Object event)
{
long time = System.currentTimeMillis();
Map<String, Object> values = new HashMap<>();
values.put("severity", severity);
KeyValueReceiver receiver = (key, v) -> {
if((v instanceof Double && Double.isNaN((Double) v))
|| (v instanceof Float && Float.isNaN((Float) v)))
{
// Skip NaN values
return;
}
values.put(key, v);
};
if(event instanceof KeyValueMappable)
{
((KeyValueMappable) event).mapToKeyValues(receiver);
}
else
{
receiver.add("value", event);
}
DataPoint point = new DataPoint(path, time, tags, values);
queue.add(point);
}
}
public static class Builder
{
private final Map<String, String> tags;
private String url;
private String username;
private String password;
private String db;
public Builder()
{
tags = new HashMap<>();
}
/**
* Set the URL of the the InfluxDB instance.
*
* @param url
* @return
*/
public Builder setUrl(String url)
{
this.url = url;
return this;
}
/**
* Set the authentication to use to connect to InfluxDB.
*
* @param username
* @param password
* @return
*/
public Builder setAuthentication(String username, String password)
{
this.username = username;
this.password = password;
return this;
}
public Builder setDatabase(String db)
{
this.db = db;
return this;
}
/**
* Add a tag to this instance. This is useful to provide information
* about the host or anything else that is shared by everything in
* this Vibe instance.
*
* @param key
* @param value
* @return
*/
public Builder addTag(String key, String value)
{
tags.put(key, value);
return this;
}
/**
* Build the instance.
*
* @return
*/
public VibeBackend build()
{
Objects.requireNonNull(url, "URL to InfluxDB is required");
Objects.requireNonNull(db, "Database to use is required");
return new InfluxDBBackend(url, username, password, db, tags);
}
}
public static Builder builder()
{
return new Builder();
}
}