- * Logback batch appender for Loggly HTTP API. + * Logback batch appender for + * Loggly HTTP API. *
- *Note:Loggly's Syslog API is much more scalable than the HTTP API which should mostly be used in - * low-volume or non-production systems. The HTTP API can be very convenient to workaround firewalls.
- *If the {@link LogglyBatchAppender} saturates and discards log messages, the following warning message is
- * appended to both Loggly and {@link System#err}:
+ *
+ * Note:Loggly's Syslog API is much more scalable than the HTTP + * API which should mostly be used in low-volume or non-production systems. The + * HTTP API can be very convenient to workaround firewalls.
+ *
+ * If the {@link LogglyBatchAppender} saturates and discards log messages, the
+ * following warning message is appended to both Loggly and {@link System#err}:
+ *
* "$date - OutputStream is full, discard previous logs"
| inputKey | *String | - *Loggly input key. "inputKey" or endpointUrl is required. Sample
- * "12345678-90ab-cdef-1234-567890abcdef" |
+ * Loggly input key. "inputKey" or endpointUrl is
+ * required. Sample "12345678-90ab-cdef-1234-567890abcdef" |
*
| endpointUrl | *String | - *Loggly HTTP API endpoint URL. "inputKey" or endpointUrl is required. Sample:
+ * | Loggly HTTP API endpoint URL. "inputKey" or
+ * endpointUrl is required. Sample:
* "https://logs.loggly.com/inputs/12345678-90ab-cdef-1234-567890abcdef" |
*
| proxyHost | *String | - *hostname of a proxy server. If blank, no proxy is used (See {@link URL#openConnection(java.net.Proxy)}. | + *hostname of a proxy server. If blank, no proxy is used (See + * {@link URL#openConnection(java.net.Proxy)}. | *
| proxyPort | *int | - *port of a proxy server. Must be a valid int but is ignored if proxyHost is blank or null. |
+ * port of a proxy server. Must be a valid int but is ignored if
+ * proxyHost is blank or null. |
*
| jmxMonitoring | *boolean | *Enable registration of a monitoring MBean named
- * "ch.qos.logback:type=LogglyBatchAppender,name=LogglyBatchAppender@#hashcode#". Default: true. |
+ * "|
| maxNumberOfBuckets | *int | - *Max number of buckets of in the byte buffer. Default value: 8. |
+ * Max number of buckets of in the byte buffer. Default value:
+ * 8. |
*
| maxBucketSizeInKilobytes | *int | - *Max size of each bucket. Default value: 1024 Kilobytes (1MB). |
+ * Max size of each bucket. Default value: 1024 Kilobytes
+ * (1MB). |
*
| flushIntervalInSeconds | *int | - *Interval of the buffer flush to Loggly API. Default value: 3. |
+ * Interval of the buffer flush to Loggly API. Default value:
+ * 3. |
*
| connReadTimeoutSeconds | *int | - *How Long the HTTP Connection will wait on reads. Default value: 1 second. |
+ * How Long the HTTP Connection will wait on reads. Default value:
+ * 1 second. |
*
flushIntervalInSeconds parameter to "2s" or event "1s".
+ * Default configuration consumes up to 8 buffers of 1024 Kilobytes (1MB) each,
+ * which seems very reasonable even for small JVMs. If logs are discarded, try
+ * first to shorten the flushIntervalInSeconds parameter to "2s" or
+ * event "1s".
*
*
@@ -137,11 +152,13 @@
* Implementation decisions
*
* - Why buffer the generated log messages as bytes instead of using the
- * {@code ch.qos.logback.core.read.CyclicBufferAppender} and buffering the {@code ch.qos.logback.classic.spi.ILoggingEvent} ?
- * Because it is much easier to control the size in memory
+ * {@code ch.qos.logback.core.read.CyclicBufferAppender} and buffering the
+ * {@code ch.qos.logback.classic.spi.ILoggingEvent} ? Because it is much easier
+ * to control the size in memory
* -
- * Why buffer in a byte array instead of directly writing in a {@link BufferedOutputStream} on the {@link HttpURLConnection} ?
- * Because the Loggly API may not like such kind of streaming approach.
+ * Why buffer in a byte array instead of directly writing in a
+ * {@link BufferedOutputStream} on the {@link HttpURLConnection} ? Because the
+ * Loggly API may not like such kind of streaming approach.
*
*
*
@@ -150,7 +167,7 @@
public class LogglyBatchAppender extends AbstractLogglyAppender implements LogglyBatchAppenderMBean {
public static final String ENDPOINT_URL_PATH = "bulk/";
-
+
private boolean debug = false;
private int flushIntervalInSeconds = 3;
@@ -192,7 +209,7 @@ protected void append(E eventObject) {
// Issue #21: Make sure messages end with new-line to delimit
// individual log events within the batch sent to loggly.
if (!msg.endsWith("\n")) {
- msg += "\n";
+ msg += "\n";
}
try {
@@ -336,44 +353,65 @@ protected HttpURLConnection getHttpConnection(URL url) throws IOException {
/**
* Send log entries to Loggly
+ *
* @param in log input stream
*/
protected void processLogEntries(InputStream in) {
long nanosBefore = System.nanoTime();
+ int currentRetryCount = 0;
+ boolean success = false;
try {
-
- HttpURLConnection conn = getHttpConnection(new URL(endpointUrl));
- /* Set connection Read Timeout */
- conn.setReadTimeout(connReadTimeoutSeconds*1000);
- BufferedOutputStream out = new BufferedOutputStream(conn.getOutputStream());
-
- long len = IoUtils.copy(in, out);
- sentBytes.addAndGet(len);
-
- out.flush();
- out.close();
-
- int responseCode = conn.getResponseCode();
- String response = super.readResponseBody(conn.getInputStream());
- switch (responseCode) {
- case HttpURLConnection.HTTP_OK:
- case HttpURLConnection.HTTP_ACCEPTED:
- sendSuccessCount.incrementAndGet();
- break;
- default:
- sendExceptionCount.incrementAndGet();
- addError("LogglyAppender server-side exception: " + responseCode + ": " + response);
- }
- // force url connection recycling
- try {
- conn.getInputStream().close();
- conn.disconnect();
- } catch (Exception e) {
- // swallow exception
+ while (!success && canRetry(currentRetryCount)) {
+ try {
+ HttpURLConnection conn = getHttpConnection(new URL(endpointUrl));
+ /* Set connection Read Timeout */
+ conn.setReadTimeout(connReadTimeoutSeconds * 1000);
+ BufferedOutputStream out = new BufferedOutputStream(conn.getOutputStream());
+
+ //If we are retrying the request, the input stream needs to be reset in order to be read again
+ if (currentRetryCount > 0) {
+ in.reset();
+ }
+
+ long len = IoUtils.copy(in, out);
+ sentBytes.addAndGet(len);
+
+ out.flush();
+ out.close();
+
+ int responseCode = conn.getResponseCode();
+ String response = super.readResponseBody(conn.getInputStream());
+ switch (responseCode) {
+ case HttpURLConnection.HTTP_OK:
+ case HttpURLConnection.HTTP_ACCEPTED:
+ success = true;
+ sendSuccessCount.incrementAndGet();
+ break;
+ default:
+ if (!canRetry(currentRetryCount)) {
+ sendExceptionCount.incrementAndGet();
+ addError("LogglyAppender server-side exception: " + responseCode + ": " + response);
+ } else if (isDebug()) {
+ addWarn("LogglyAppender server-side exception - Retrying...: " + responseCode + ": " + response);
+ }
+ }
+ // force url connection recycling
+ try {
+ conn.getInputStream().close();
+ conn.disconnect();
+ } catch (Exception e) {
+ // swallow exception
+ }
+ } catch (Exception e) {
+ if (!canRetry(currentRetryCount)) {
+ sendExceptionCount.incrementAndGet();
+ addError("LogglyAppender client-side exception", e);
+ } else if (isDebug()) {
+ addWarn("LogglyAppender client-side exception - Retrying...", e);
+ }
+ }
+ currentRetryCount++;
}
- } catch (Exception e) {
- sendExceptionCount.incrementAndGet();
- addError("LogglyAppender client-side exception", e);
} finally {
sendDurationInNanos.addAndGet(System.nanoTime() - nanosBefore);
}
@@ -449,17 +487,18 @@ public void setConnReadTimeoutSeconds(int connReadTimeoutSeconds) {
}
private String getDebugInfo() {
- return "{" +
- "sendDurationInMillis=" + TimeUnit.MILLISECONDS.convert(sendDurationInNanos.get(), TimeUnit.NANOSECONDS) +
- ", sendSuccessCount=" + sendSuccessCount +
- ", sendExceptionCount=" + sendExceptionCount +
- ", sentBytes=" + sentBytes +
- ", discardedBucketsCount=" + getDiscardedBucketsCount() +
- ", currentLogEntriesBufferSizeInBytes=" + getCurrentLogEntriesBufferSizeInBytes() +
- '}';
+ return "{"
+ + "sendDurationInMillis=" + TimeUnit.MILLISECONDS.convert(sendDurationInNanos.get(), TimeUnit.NANOSECONDS)
+ + ", sendSuccessCount=" + sendSuccessCount
+ + ", sendExceptionCount=" + sendExceptionCount
+ + ", sentBytes=" + sentBytes
+ + ", discardedBucketsCount=" + getDiscardedBucketsCount()
+ + ", currentLogEntriesBufferSizeInBytes=" + getCurrentLogEntriesBufferSizeInBytes()
+ + '}';
}
public class LogglyExporter implements Runnable {
+
@Override
public void run() {
try {