001package net.zileo.logback.logdna; 002 003import java.net.InetAddress; 004import java.net.UnknownHostException; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010import java.util.Map.Entry; 011import java.util.concurrent.TimeUnit; 012 013import javax.ws.rs.client.Client; 014import javax.ws.rs.client.ClientBuilder; 015import javax.ws.rs.client.Entity; 016import javax.ws.rs.client.WebTarget; 017import javax.ws.rs.core.MediaType; 018import javax.ws.rs.core.MultivaluedHashMap; 019import javax.ws.rs.core.MultivaluedMap; 020import javax.ws.rs.core.Response; 021 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import com.fasterxml.jackson.annotation.JsonInclude; 026import com.fasterxml.jackson.core.JsonProcessingException; 027import com.fasterxml.jackson.databind.DeserializationFeature; 028import com.fasterxml.jackson.databind.ObjectMapper; 029import com.fasterxml.jackson.databind.PropertyNamingStrategies; 030 031import ch.qos.logback.classic.encoder.PatternLayoutEncoder; 032import ch.qos.logback.classic.spi.ILoggingEvent; 033import ch.qos.logback.core.UnsynchronizedAppenderBase; 034 035/** 036 * Logback appender for sending logs to <a href="https://logdna.com">LogDNA.com</a>. 037 * 038 * @author jlannoy 039 */ 040public class LogDnaAppender extends UnsynchronizedAppenderBase<ILoggingEvent> { 041 042 private static final String CUSTOM_USER_AGENT = "LogDna Logback Appender"; 043 044 private final Logger errorLog = LoggerFactory.getLogger(LogDnaAppender.class); 045 046 private final ObjectMapper dataMapper; 047 048 private final ObjectMapper responseMapper; 049 050 private Client client; 051 052 private boolean disabled; 053 054 protected final MultivaluedMap<String, Object> headers; 055 056 // Assignable fields 057 058 protected String hostname; 059 060 protected PatternLayoutEncoder encoder; 061 062 protected String appName; 063 064 protected String ingestUrl = "https://logs.logdna.com/logs/ingest"; 065 066 protected List<String> mdcFields = new ArrayList<>(); 067 068 protected List<String> mdcTypes = new ArrayList<>(); 069 070 protected String tags; 071 072 protected long connectTimeout = 0; 073 074 protected long readTimeout = 0; 075 076 protected boolean useTimeDrift = true; 077 078 /** 079 * Appender initialization. 080 */ 081 public LogDnaAppender() { 082 this.headers = new MultivaluedHashMap<>(); 083 this.headers.add("User-Agent", CUSTOM_USER_AGENT); 084 this.headers.add("Accept", MediaType.APPLICATION_JSON); 085 this.headers.add("Content-Type", MediaType.APPLICATION_JSON); 086 087 this.dataMapper = new ObjectMapper(); 088 this.dataMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 089 this.dataMapper.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE); 090 091 this.responseMapper = new ObjectMapper(); 092 this.responseMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 093 } 094 095 private String identifyHostname() { 096 try { 097 return InetAddress.getLocalHost().getHostName(); 098 } catch (UnknownHostException e) { 099 return "localhost"; 100 } 101 } 102 103 // Postpone client initialization to allow timeouts configuration 104 protected Client client() { 105 if (this.client == null) { 106 107 if (this.hostname == null) { 108 this.hostname = identifyHostname(); 109 } 110 111 this.client = ClientBuilder.newBuilder() // 112 .connectTimeout(this.connectTimeout, TimeUnit.MILLISECONDS) // 113 .readTimeout(this.readTimeout, TimeUnit.MILLISECONDS) // 114 .build(); 115 } 116 117 return this.client; 118 } 119 120 /** 121 * @see ch.qos.logback.core.UnsynchronizedAppenderBase#append(java.lang.Object) 122 */ 123 @Override 124 protected void append(ILoggingEvent event) { 125 126 if (disabled) { 127 return; 128 } 129 130 if (event.getLoggerName().equals(LogDnaAppender.class.getName())) { 131 return; 132 } 133 134 if (!this.headers.containsKey("apikey") || this.headers.getFirst("apikey").toString().trim().length() == 0) { 135 errorLog.warn("Empty ingest API key for LogDNA ; disabling LogDnaAppender"); 136 this.disabled = true; 137 return; 138 } 139 140 try { 141 String jsonData = convertLogEventToJson(event); 142 143 Response response = callIngestApi(jsonData); 144 145 if (response.getStatus() != 200) { 146 LogDnaResponse logDnaResponse = convertResponseToObject(response); 147 errorLog.error("Error calling LogDna : {} ({})", logDnaResponse.getError(), response.getStatus()); 148 } 149 150 } catch (JsonProcessingException e) { 151 errorLog.error("Error processing JSON data : {}", e.getMessage()); 152 153 } catch (Exception e) { 154 errorLog.error("Error trying to call LogDna : {}", e.getMessage()); 155 } 156 157 } 158 159 protected String convertLogEventToJson(ILoggingEvent event) throws JsonProcessingException { 160 return this.dataMapper.writeValueAsString(buildPostData(event)); 161 } 162 163 protected LogDnaResponse convertResponseToObject(Response response) throws JsonProcessingException { 164 return this.responseMapper.readValue(response.readEntity(String.class), LogDnaResponse.class); 165 } 166 167 /** 168 * Call LogDna API posting given JSON formated string. 169 * 170 * @param jsonData 171 * a json oriented map 172 * @return the http response 173 */ 174 175 protected Response callIngestApi(String jsonData) { 176 WebTarget wt = client().target(ingestUrl) // 177 .queryParam("hostname", this.hostname) // 178 .queryParam("tags", tags); 179 180 if (useTimeDrift) { 181 wt = wt.queryParam("now", System.currentTimeMillis()); 182 } 183 184 return wt.request() // 185 .headers(headers) // 186 .post(Entity.json(jsonData)); 187 } 188 189 /** 190 * Converts a logback logging event to a JSON oriented map. 191 * 192 * @param event 193 * the logging event 194 * @return a json oriented map 195 */ 196 protected Map<String, Object> buildPostData(ILoggingEvent event) { 197 Map<String, Object> line = new HashMap<>(); 198 line.put("timestamp", event.getTimeStamp()); 199 line.put("level", event.getLevel().toString()); 200 line.put("app", this.appName); 201 line.put("line", this.encoder != null ? new String(this.encoder.encode(event)) : event.getFormattedMessage()); 202 203 Map<String, Object> meta = new HashMap<>(); 204 meta.put("logger", event.getLoggerName()); 205 if (!mdcFields.isEmpty() && !event.getMDCPropertyMap().isEmpty()) { 206 for (Entry<String, String> entry : event.getMDCPropertyMap().entrySet()) { 207 if (mdcFields.contains(entry.getKey())) { 208 String type = mdcTypes.get(mdcFields.indexOf(entry.getKey())); 209 meta.put(entry.getKey(), getMetaValue(type, entry.getValue())); 210 } 211 } 212 } 213 line.put("meta", meta); 214 215 Map<String, Object> lines = new HashMap<>(); 216 lines.put("lines", Arrays.asList(line)); 217 return lines; 218 } 219 220 private Object getMetaValue(String type, String value) { 221 try { 222 if ("int".equals(type)) { 223 return Integer.valueOf(value); 224 } 225 if ("long".equals(type)) { 226 return Long.valueOf(value); 227 } 228 if ("boolean".equals(type)) { 229 return Boolean.valueOf(value); 230 } 231 } catch (NumberFormatException e) { 232 errorLog.warn("Error getting meta value : {}", e.getMessage()); 233 } 234 return value; 235 236 } 237 238 public void setEncoder(PatternLayoutEncoder encoder) { 239 this.encoder = encoder; 240 } 241 242 /** 243 * Sets the application name for LogDNA indexation. 244 * 245 * @param appName 246 * application name 247 */ 248 public void setAppName(String appName) { 249 this.appName = appName; 250 } 251 252 /** 253 * Sets the LogDNA ingest API url. 254 * 255 * @param ingestUrl 256 * logdna url 257 */ 258 public void setIngestUrl(String ingestUrl) { 259 this.ingestUrl = ingestUrl; 260 } 261 262 /** 263 * Sets your LogDNA ingest API key. 264 * 265 * @param ingestKey 266 * your ingest key 267 */ 268 public void setIngestKey(String ingestKey) { 269 this.headers.add("apikey", ingestKey); 270 } 271 272 /** 273 * Sets the MDC fields that needs to be sent inside LogDNA metadata, separated by a comma. 274 * 275 * @param mdcFields 276 * MDC fields to use 277 */ 278 public void setMdcFields(String mdcFields) { 279 this.mdcFields = Arrays.asList(mdcFields.split(",")); 280 } 281 282 /** 283 * Sets the MDC fields types that will be sent inside LogDNA metadata, in the same order as <i>mdcFields</i> are set 284 * up, separated by a comma. Possible values are <i>string</i>, <i>boolean</i>, <i>int</i> and <i>long</i>. The last 285 * two will result as an indexed <i>number</i> in LogDNA's console. 286 * 287 * @param mdcTypes 288 * MDC fields types 289 */ 290 public void setMdcTypes(String mdcTypes) { 291 this.mdcTypes = Arrays.asList(mdcTypes.split(",")); 292 } 293 294 /** 295 * Sets the tags that needs to be sent to LogDNA, for grouping hosts for example. 296 * 297 * @param tags 298 * fixed tags 299 */ 300 public void setTags(String tags) { 301 this.tags = tags; 302 } 303 304 /** 305 * Set whether using time drift. If set true, now parameter is supplied (https://docs.logdna.com/reference). 306 * 307 * @param useTimeDrift 308 * true: Use time drift. false: Do not use time drift. 309 */ 310 public void setUseTimeDrift(String useTimeDrift) { 311 this.useTimeDrift = !useTimeDrift.equalsIgnoreCase("false"); 312 } 313 314 /** 315 * Force a given value for the hostname LogDNA parameter. 316 * 317 * @param hostname 318 * local hostname value 319 */ 320 public void setHostname(String hostname) { 321 this.hostname = hostname; 322 } 323 324 /** 325 * Sets the connection timeout of the underlying HTTP client, in milliseconds. 326 * 327 * @param connectTimeout 328 * client connection timeout 329 */ 330 public void setConnectTimeout(Long connectTimeout) { 331 this.connectTimeout = connectTimeout; 332 } 333 334 /** 335 * Sets the read timeout of the underlying HTTP client, in milliseconds. 336 * 337 * @param readTimeout 338 * client read timeout 339 */ 340 public void setReadTimeout(Long readTimeout) { 341 this.readTimeout = readTimeout; 342 } 343 344 public boolean isDisabled() { 345 return this.disabled; 346 } 347}