/**
* Copyright 2013 Michael K. Werle
*
* 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 com.coruscations.logback.redis.logstash;
import com.coruscations.logback.redis.RedisAppenderBase;
import com.lmax.disruptor.EventHandler;
import org.slf4j.Marker;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import redis.clients.jedis.Jedis;
public class RedisLogstashAppender extends RedisAppenderBase<ILoggingEvent, String> {
private static final String[] ESCAPE_STRINGS = new String[]{
"\\\\u0000", "\\\\u0001", "\\\\u0002", "\\\\u0003", "\\\\u0004", "\\\\u0005", "\\\\u0006",
"\\\\u0007", "\\\\b", "\\\\t", "\\\\n", "\\\\u000B", "\\\\f", "\\\\r", "\\\\u000E",
"\\\\u000F", "\\\\u0010", "\\\\u0011", "\\\\u0012", "\\\\u0013", "\\\\u0014", "\\\\u0015",
"\\\\u0016", "\\\\u0017", "\\\\u0018", "\\\\u0019", "\\\\u001A", "\\\\u001B", "\\\\u001C",
"\\\\u001D", "\\\\u001E", "\\\\u001F"};
boolean includeCallerData = false;
// Logstash information
private String key = "logstash";
private String type = "";
private String hostName = null;
private String file = "logback";
private String source;
public RedisLogstashAppender() {
String hostName = null;
try {
hostName = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
// Ignore
}
if (hostName == null) {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
// Prefer IPv4 addresses
InetAddress inetAddress = null;
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
List<InterfaceAddress> interfaceAddresses = networkInterface.getInterfaceAddresses();
for (InterfaceAddress interfaceAddress : interfaceAddresses) {
inetAddress = interfaceAddress.getAddress();
if (inetAddress instanceof Inet4Address) {
break;
}
}
}
if (inetAddress != null) {
hostName = inetAddress.getHostName();
}
} catch (SocketException e) {
// give up
}
}
this.hostName = hostName;
updateSource();
}
private final ThreadLocal<ISO8601Formatter> iso8601DateFormat =
new ThreadLocal<ISO8601Formatter>() {
@Override
protected ISO8601Formatter initialValue() {
return new ISO8601Formatter();
}
};
@Override
public EventHandler<EventWrapper<String>> getEventFlusher() {
return new LogstashEventFlusher();
}
public boolean isIncludeCallerData() {
return includeCallerData;
}
public void setIncludeCallerData(boolean includeCallerData) {
this.includeCallerData = includeCallerData;
}
public String getKey() {
return key;
}
public void setKey(String key) {
if (key == null || key.length() == 0) {
throw new IllegalArgumentException("Key cannot be null or empty.");
}
StringBuilder sb = new StringBuilder(key.length());
escape(key, sb);
this.key = sb.toString();
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type == null ? "" : type;
updateSource();
}
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
updateSource();
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
updateSource();
}
private void updateSource() {
StringBuilder sb = new StringBuilder(127);
escape((type.length() == 0 ? "" : (type + "://")) +
hostName + "/" +
(file == null ? "logback" : file), sb);
this.source = sb.toString();
}
@Override
public String formatEvent(ILoggingEvent event) {
StringBuilder sb = new StringBuilder(2047);
sb.append("{\"@source\":\"");
escape(source, sb);
sb.append("\",");
sb.append("\"@tags\":[");
appendTags(sb, event);
sb.append("],");
sb.append("\"@fields\":{");
appendFields(sb, event);
sb.append("},");
String iso8601Date = iso8601DateFormat.get().format(event.getTimeStamp());
sb.append("\"@timestamp\":\"").append(iso8601Date).append("\",");
sb.append("\"@message\":\"");
String formattedMessage = event.getFormattedMessage();
if (formattedMessage == null) {
formattedMessage = "";
}
escape(formattedMessage, sb);
sb.append("\",");
sb.append("\"@type\":\"").append(type).append("\"}");
return sb.toString();
}
void appendTags(StringBuilder sb, ILoggingEvent event) {
boolean first = true;
Marker marker = event.getMarker();
if (marker != null) {
sb.append("\"");
escape(marker.getName(), sb);
sb.append("\"");
first = false;
}
String tags = event.getMDCPropertyMap().get("tags");
if (tags != null) {
// TODO: Consider avoiding split() because of regex cost
for (String tag : tags.split("(?m),")) {
if (tag == null || tag.length() <= 0) {
continue;
}
if (!first) {
sb.append(',');
}
first = false;
sb.append("\"");
escape(tag.trim(), sb);
sb.append("\"");
}
}
}
private void appendFields(StringBuilder sb, ILoggingEvent event) {
// Start with things we might not always have
IThrowableProxy throwableProxy = event.getThrowableProxy();
if (throwableProxy != null) {
appendField(sb, "stack_trace", ThrowableProxyUtil.asString(throwableProxy)).append(',');
}
Map<String, String> mdc = event.getMDCPropertyMap();
for (Map.Entry<String, String> entry : mdc.entrySet()) {
appendField(sb, entry.getKey(), entry.getValue()).append(',');
}
// We always have these
appendField(sb, "logger_name", event.getLoggerName()).append(',');
appendField(sb, "thread_name", event.getThreadName()).append(',');
// Is there any value to this?
//appendField(sb, "level_value", String.valueOf(event.getLevel().toInt())).append(',');
appendField(sb, "level", event.getLevel().toString());
}
private StringBuilder appendField(StringBuilder sb, String name, String value) {
sb.append('"');
escape(name, sb);
// sb.append("\":[\"");
sb.append("\":\"");
escape(value, sb);
// sb.append("\"]");
sb.append('"');
return sb;
}
@SuppressWarnings("ImplicitNumericConversion")
private static void escape(String input, StringBuilder sb) {
for (int i = 0, length = input.length(); i < length; i++) {
char ch = input.charAt(i);
switch (ch) {
case '\\':
sb.append("\\\\\\\\");
break;
case '"':
sb.append("\\\"");
break;
default:
if (ch < 0x20) {
sb.append(ESCAPE_STRINGS[ch]);
} else {
sb.append(ch);
}
}
}
}
private class LogstashEventFlusher
implements EventHandler<EventWrapper<String>> {
private final List<String> jsonStrings = new LinkedList<String>();
@Override
public void onEvent(EventWrapper<String> event, long sequence, boolean endOfBatch) {
jsonStrings.add(event.getMessage());
if (endOfBatch) {
String[] values = new String[jsonStrings.size()];
Jedis jedis = pool.getResource();
try {
jedis.rpush(key, jsonStrings.toArray(values));
} catch (Exception e) {
addError("Failed to flush " + jsonStrings.size() + "log messages to " +
getRedisHostName() + ":" + getRedisPort());
} finally {
pool.returnResource(jedis);
}
// Clear regardless of success to we do not leak memory.
jsonStrings.clear();
}
}
}
private class ISO8601Formatter {
DateFormat dateFormat;
Date date;
private ISO8601Formatter() {
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
date = new Date();
}
private String format(long timestamp) {
date.setTime(timestamp);
return dateFormat.format(date);
}
}
}