package com.newrelic.apm.enterprise.log; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Queue; import java.util.concurrent.*; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; /** * A handler for JDK logging that sends JSON-formatted messages to Loggly. */ public class LogglyHandler extends Handler { private HttpClient httpClient; private ThreadPoolExecutor pool; private Queue<LogglySample> retryQueue; private boolean allowRetry = true; private String inputUrl; /** * Creates a handler with the specified input URL, 10 threads, and support for 5000 messages in the backlog. * * @param inputUrl - the URL provided by Loggly for sending log messages to * @see #LogglyHandler(String, int, int) */ public LogglyHandler(String inputUrl) { this(inputUrl, 10, 5000); } /** * Creates a handler with the specified input URL, max thread count, and message backlog support. * * @param inputUrl - the URL provided by Loggly for sending log messages to * @param maxThreads - the max number of concurrent background threads that are allowed to send data to Loggly * @param backlog - the max number of log messages that can be queued up (anything beyond will be thrown away) */ public LogglyHandler(String inputUrl, int maxThreads, int backlog) { this.inputUrl = inputUrl; pool = new ThreadPoolExecutor(maxThreads, maxThreads, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(backlog), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "Loggly Thread"); thread.setDaemon(true); return thread; } }, new ThreadPoolExecutor.DiscardOldestPolicy()); pool.allowCoreThreadTimeOut(true); retryQueue = new LinkedBlockingQueue<>(backlog); Thread retryThread = new Thread(new Runnable() { @Override public void run() { while (allowRetry) { // drain the retry requests LogglySample sample = null; while ((sample = retryQueue.poll()) != null) { if (sample.retryCount > 10) { // todo: capture statistics about the failure (exception and/or status code) // and then report on it in some sort of thoughtful way to standard err } else { pool.submit(sample); } } // retry every 10 seconds try { Thread.sleep(10000); } catch (InterruptedException e) { System.err.println("Retry sleep was interrupted, giving up on retry thread"); return; } } } }, "Loggly Retry Thread"); retryThread.setDaemon(true); retryThread.start(); httpClient = HttpClientBuilder.create() .setMaxConnPerRoute(maxThreads) .setMaxConnTotal(maxThreads) .build(); // because the threads a daemon threads, we want to give them a chance // to finish up before we totally shut down Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { close(); } })); } @Override public void publish(LogRecord record) { StringWriter sw = new StringWriter(); String level = "UNKN"; if (record.getLevel().equals(Level.WARNING)) { level = "WARN"; } else if (record.getLevel().equals(Level.SEVERE)) { level = "SEVR"; } else if (record.getLevel().equals(Level.INFO)) { level = "INFO"; } else if (record.getLevel().equals(Level.FINE)) { level = "FINE"; } else if (record.getLevel().equals(Level.FINEST)) { level = "FNST"; } else if (record.getLevel().equals(Level.FINER)) { level = "FINR"; } else if (record.getLevel().equals(Level.CONFIG)) { level = "CONF"; } else if (record.getLevel().equals(Level.OFF)) { level = "OFF "; } else if (record.getLevel().equals(Level.ALL)) { level = "ALL "; } sw.append(level).append(' '); // and the log message itself if (record.getParameters() != null && record.getParameters().length > 0) { java.util.Formatter formatter = new java.util.Formatter(); formatter.format(record.getMessage(), record.getParameters()); sw.append(formatter.toString()); } else { sw.append(record.getMessage()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") Throwable thrown = record.getThrown(); if (thrown != null) { thrown.printStackTrace(new PrintWriter(sw)); } pool.submit(new LogglySample(sw.toString())); } @Override public void flush() { } @Override public synchronized void close() throws SecurityException { if (pool.isShutdown()) { return; } try { // first, anything in the retry queue should be tried one last time and then we give up on it allowRetry = false; for (LogglySample sample : retryQueue) { pool.submit(sample); } retryQueue.clear(); System.out.println("Shutting down Loggly handler - waiting 90 seconds for " + pool.getQueue().size() + " logs to finish"); pool.shutdown(); try { boolean result = pool.awaitTermination(90, TimeUnit.SECONDS); if (!result) { System.out.println("Not all Loggly messages sent out - still had " + pool.getQueue().size() + " left :("); pool.shutdownNow(); } } catch (InterruptedException e) { // ignore } } finally { httpClient.getConnectionManager().shutdown(); System.out.println("Loggly handler shut down"); } } private class LogglySample implements Runnable { private String msg; private int retryCount = 0; private Exception exception; private int statusCode; public LogglySample(String msg) { this(msg, 0); } private LogglySample(String msg, int retryCount) { this.msg = msg; this.retryCount = retryCount; } @Override public void run() { HttpEntity entity = null; try { HttpPost post = new HttpPost(inputUrl); post.setEntity(new StringEntity(msg)); post.setHeader("Content-Type", "text/plain"); HttpResponse response = httpClient.execute(post); entity = response.getEntity(); statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { if (allowRetry) { retryCount++; retryQueue.offer(this); } } } catch (Exception e) { if (allowRetry) { exception = e; retryCount++; retryQueue.offer(this); } } finally { if (entity != null) { try { entity.getContent().close(); } catch (IOException e) { // safe to ignore } } } } } }