├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── internetitem │ └── logback │ └── elasticsearch │ ├── AbstractElasticsearchAppender.java │ ├── AbstractElasticsearchPublisher.java │ ├── AccessElasticsearchPublisher.java │ ├── ClassicElasticsearchPublisher.java │ ├── ElasticsearchAccessAppender.java │ ├── ElasticsearchAppender.java │ ├── ElasticsearchOutputAggregator.java │ ├── PropertySerializer.java │ ├── config │ ├── AWSAuthentication.java │ ├── Authentication.java │ ├── BasicAuthentication.java │ ├── ElasticsearchProperties.java │ ├── HttpRequestHeader.java │ ├── HttpRequestHeaders.java │ ├── Property.java │ └── Settings.java │ ├── util │ ├── AbstractPropertyAndEncoder.java │ ├── AccessPropertyAndEncoder.java │ ├── Base64.java │ ├── ClassicPropertyAndEncoder.java │ └── ErrorReporter.java │ └── writer │ ├── ElasticsearchWriter.java │ ├── LoggerWriter.java │ ├── SafeWriter.java │ └── StdErrWriter.java └── test └── java └── com └── internetitem └── logback └── elasticsearch ├── ElasticsearchAppenderTest.java └── PropertySerializerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.project 3 | /.classpath 4 | /.settings/ 5 | /target/ 6 | /bin/ 7 | 8 | *.iml 9 | *.jar 10 | *.class 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | ------- 3 | 4 | logback-elasticsearch-appender 5 | Copyright (C) 2015, Adam Batkin. All rights reserved. 6 | 7 | This program and the accompanying materials are dual-licensed under 8 | either the terms of the Eclipse Public License v1.0 as published by 9 | the Eclipse Foundation 10 | 11 | or (per the licensee's choosing) 12 | 13 | under the terms of the GNU Lesser General Public License version 2.1 14 | as published by the Free Software Foundation. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Logback Elasticsearch Appender 2 | =============================== 3 | 4 | [![Build Status](https://travis-ci.org/internetitem/logback-elasticsearch-appender.svg?branch=master)](https://travis-ci.org/internetitem/logback-elasticsearch-appender) 5 | 6 | Send log events directly from Logback to Elasticsearch. Logs are delivered asynchronously (i.e. not on the main thread) so will not block execution of the program. Note that the queue backlog can be bounded and messages *can* be lost if Elasticsearch is down and either the backlog queue is full or the producer program is trying to exit (it will retry up to a configured number of attempts, but will not block shutdown of the program beyond that). For long-lived programs, this should not be a problem, as messages should be delivered eventually. 7 | 8 | This software is dual-licensed under the EPL 1.0 and LGPL 2.1, which is identical to the [Logback License](http://logback.qos.ch/license.html) itself. 9 | 10 | Usage 11 | ===== 12 | Include slf4j and logback as usual (depending on this library will *not* automatically pull them in). 13 | 14 | In your `pom.xml` (or equivalent), add: 15 | 16 | 17 | com.internetitem 18 | logback-elasticsearch-appender 19 | 1.6 20 | 21 | 22 | In your `logback.xml`: 23 | 24 | 25 | http://yourserver/_bulk 26 | logs-%date{yyyy-MM-dd} 27 | tester 28 | es-logger 29 | es-error-logger 30 | 30000 31 | false 32 | false 33 | false 34 | 104857600 35 | 3 36 | 30000 37 | 250 38 | false 39 | false 40 | 100 41 | 42 | 43 | 44 | host 45 | ${HOSTNAME} 46 | false 47 | 48 | 49 | severity 50 | %level 51 | 52 | 53 | thread 54 | %thread 55 | 56 | 57 | stacktrace 58 | %ex 59 | 60 | 61 | logger 62 | %logger 63 | 64 | 65 | 66 |
67 | Content-Type 68 | application/json 69 |
70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | %msg 87 | 88 | 89 | 90 | 91 | 92 | 93 | Configuration Reference 94 | ======================= 95 | 96 | * `url` (required): The URL to your Elasticsearch bulk API endpoint 97 | * `index` (required): Name if the index to publish to (populated using PatternLayout just like individual properties - see below) 98 | * `type` (optional): Elasticsearch `_type` field for records. Although this library does not require `type` to be populated, Elasticsearch may, unless the configured URL includes the type (i.e. `{index}/{type}/_bulk` as opposed to `/_bulk` and `/{index}/_bulk`). See the Elasticsearch [Bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html) documentation for more information 99 | * `sleepTime` (optional, default 250): Time (in ms) to sleep between attempts at delivering a message 100 | * `maxRetries` (optional, default 3): Number of times to attempt retrying a message on failure. Note that subsequent log messages reset the retry count to 0. This value is important if your program is about to exit (i.e. it is not producing any more log lines) but is unable to deliver some messages to ES 101 | * `connectTimeout` (optional, default 30000): Elasticsearch connect timeout (in ms) 102 | * `readTimeout` (optional, default 30000): Elasticsearch read timeout (in ms) 103 | * `includeCallerData` (optional, default false): If set to `true`, save the caller data (identical to the [AsyncAppender's includeCallerData](http://logback.qos.ch/manual/appenders.html#asyncIncludeCallerData)) 104 | * `errorsToStderr` (optional, default false): If set to `true`, any errors in communicating with Elasticsearch will also be dumped to stderr (normally they are only reported to the internal Logback Status system, in order to prevent a feedback loop) 105 | * `logsToStderr` (optional, default false): If set to `true`, dump the raw Elasticsearch messages to stderr 106 | * `maxQueueSize` (optional, default 104,857,600 = 200MB): Maximum size (in characters) of the send buffer. After this point, *logs will be dropped*. This should only happen if Elasticsearch is down, but this is a self-protection mechanism to ensure that the logging system doesn't cause the main process to run out of memory. Note that this maximum is approximate; once the maximum is hit, no new logs will be accepted until it shrinks, but any logs already accepted to be processed will still be added to the buffer 107 | * `loggerName` (optional): If set, raw ES-formatted log data will be sent to this logger 108 | * `errorLoggerName` (optional): If set, any internal errors or problems will be logged to this logger 109 | * `rawJsonMessage` (optional, default false): If set to `true`, the log message is interpreted as pre-formatted raw JSON message. 110 | * `includeMdc` (optional, default false): If set to `true`, then all [MDC](http://www.slf4j.org/api/org/slf4j/MDC.html) values will be mapped to properties on the JSON payload. 111 | * `maxMessageSize` (optional, default -1): If set to a number greater than 0, truncate messages larger than this length, then append "`..`" to denote that the message was truncated 112 | * `authentication` (optional): Add the ability to send authentication headers (see below) 113 | 114 | The fields `@timestamp` and `message` are always sent and can not currently be configured. Additional fields can be sent by adding `` elements to the `` set. 115 | 116 | * `name` (required): Key to be used in the log event 117 | * `value` (required): Text string to be sent. Internally, the value is populated using a Logback PatternLayout, so all [Conversion Words](http://logback.qos.ch/manual/layouts.html#conversionWord) can be used (in addition to the standard static variable interpolations like `${HOSTNAME}`). 118 | * `allowEmpty` (optional, default `false`): Normally, if the `value` results in a `null` or empty string, the field will not be sent. If `allowEmpty` is set to `true` then the field will be sent regardless 119 | * `type` (optional, default `String`): type of the field on the resulting JSON message. Possible values are: `String`, `int`, `float` and `boolean`. 120 | 121 | Groovy Configuration 122 | ==================== 123 | 124 | If you configure logback using `logback.groovy`, this can be configured as follows: 125 | 126 | import com.internetitem.logback.elasticsearch.ElasticsearchAppender 127 | 128 | appender("ELASTIC", ElasticsearchAppender){ 129 | url = 'http://yourserver/_bulk' 130 | index = 'logs-%date{yyyy-MM-dd}' 131 | type = 'log' 132 | rawJsonMessage = true 133 | errorsToStderr = true 134 | authentication = new BasicAuthentication() 135 | def configHeaders = new HttpRequestHeaders() 136 | configHeaders.addHeader(new HttpRequestHeader(name: 'Content-Type', value: 'text/plain')) 137 | headers = configHeaders 138 | } 139 | 140 | root(INFO, ["ELASTIC"]) 141 | 142 | Authentication 143 | ============== 144 | 145 | Authentication is a pluggable mechanism. You must specify the authentication class on the XML element itself. The currently supported classes are: 146 | 147 | * `com.internetitem.logback.elasticsearch.config.BasicAuthentication` - Username and password are taken from the URL (i.e. `http://username:password@yourserver/_bulk`) 148 | * `com.internetitem.logback.elasticsearch.config.AWSAuthentication` - Authenticate using the AWS SDK, for use with the [Amazon Elasticsearch Service](https://aws.amazon.com/elasticsearch-service/) (note that you will also need to include `com.amazonaws:aws-java-sdk-core` as a dependency) 149 | 150 | Logback Access 151 | ============== 152 | 153 | Included is also an Elasticsearch appender for Logback Access. The configuration is almost identical, with the following two differences: 154 | 155 | * The Appender class name is `com.internetitem.logback.elasticsearch.ElasticsearchAccessAppender` 156 | * The `value` for each `property` uses the [Logback Access conversion words](http://logback.qos.ch/manual/layouts.html#logback-access). 157 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.internetitem 6 | logback-elasticsearch-appender 7 | 1.7-SNAPSHOT 8 | jar 9 | 10 | Logback Elasticsearch Appender 11 | Send log events directly from Logback to Elasticsearch 12 | https://github.com/internetitem/logback-elasticsearch-appender 13 | 14 | 15 | 16 | The MIT License (MIT) 17 | http://opensource.org/licenses/MIT 18 | repo 19 | 20 | 21 | 22 | 23 | Internet Item 24 | https://internetitem.com/ 25 | 26 | 27 | 28 | scm:git:https://github.com/internetitem/logback-elasticsearch-appender.git 29 | scm:git:https://github.com/internetitem/logback-elasticsearch-appender.git 30 | https://github.com/internetitem/logback-elasticsearch-appender 31 | HEAD 32 | 33 | 34 | 35 | 36 | Adam Batkin 37 | adam@batkin.net 38 | 39 | 40 | 41 | 42 | UTF-8 43 | UTF-8 44 | 45 | 46 | 47 | 48 | ch.qos.logback 49 | logback-classic 50 | 1.1.7 51 | provided 52 | 53 | 54 | ch.qos.logback 55 | logback-access 56 | 1.1.7 57 | provided 58 | 59 | 60 | org.slf4j 61 | slf4j-api 62 | 1.7.21 63 | provided 64 | 65 | 66 | com.fasterxml.jackson.core 67 | jackson-core 68 | 2.8.0 69 | 70 | 71 | com.amazonaws 72 | aws-java-sdk-core 73 | 1.11.31 74 | provided 75 | 76 | 77 | junit 78 | junit 79 | 4.12 80 | test 81 | 82 | 83 | org.mockito 84 | mockito-all 85 | 1.10.19 86 | test 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-release-plugin 95 | 2.5.3 96 | 97 | v@{project.version} 98 | false 99 | release 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-compiler-plugin 105 | 3.5.1 106 | 107 | 1.7 108 | 1.7 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | release 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-source-plugin 122 | 3.0.1 123 | 124 | 125 | attach-sources 126 | 127 | jar-no-fork 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-javadoc-plugin 135 | 2.10.4 136 | 137 | 138 | attach-javadocs 139 | 140 | jar 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-gpg-plugin 148 | 1.6 149 | 150 | 151 | sign-artifacts 152 | verify 153 | 154 | sign 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | ossrh 167 | https://oss.sonatype.org/content/repositories/snapshots 168 | 169 | 170 | ossrh 171 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/AbstractElasticsearchAppender.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import java.io.IOException; 4 | import java.net.MalformedURLException; 5 | import java.net.URL; 6 | 7 | import ch.qos.logback.core.UnsynchronizedAppenderBase; 8 | import com.internetitem.logback.elasticsearch.config.Authentication; 9 | import com.internetitem.logback.elasticsearch.config.ElasticsearchProperties; 10 | import com.internetitem.logback.elasticsearch.config.HttpRequestHeaders; 11 | import com.internetitem.logback.elasticsearch.config.Settings; 12 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 13 | 14 | public abstract class AbstractElasticsearchAppender extends UnsynchronizedAppenderBase { 15 | 16 | protected Settings settings; 17 | protected ElasticsearchProperties elasticsearchProperties; 18 | protected AbstractElasticsearchPublisher publisher; 19 | protected ErrorReporter errorReporter; 20 | protected HttpRequestHeaders headers; 21 | 22 | public AbstractElasticsearchAppender() { 23 | this.settings = new Settings(); 24 | this.headers = new HttpRequestHeaders(); 25 | } 26 | 27 | public AbstractElasticsearchAppender(Settings settings) { 28 | this.settings = settings; 29 | } 30 | 31 | @Override 32 | public void start() { 33 | super.start(); 34 | this.errorReporter = getErrorReporter(); 35 | try { 36 | this.publisher = buildElasticsearchPublisher(); 37 | } catch (IOException e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | protected void publishEvent(T eventObject) { 43 | publisher.addEvent(eventObject); 44 | } 45 | 46 | //VisibleForTesting 47 | protected ErrorReporter getErrorReporter() { 48 | return new ErrorReporter(settings, getContext()); 49 | } 50 | 51 | //VisibleForTesting 52 | protected abstract AbstractElasticsearchPublisher buildElasticsearchPublisher() throws IOException; 53 | 54 | @Override 55 | public void stop() { 56 | super.stop(); 57 | } 58 | 59 | @Override 60 | protected void append(T eventObject) { 61 | appendInternal(eventObject); 62 | } 63 | 64 | protected abstract void appendInternal(T eventObject); 65 | 66 | public void setProperties(ElasticsearchProperties elasticsearchProperties) { 67 | this.elasticsearchProperties = elasticsearchProperties; 68 | } 69 | 70 | public void setSleepTime(int sleepTime) { 71 | settings.setSleepTime(sleepTime); 72 | } 73 | 74 | public void setMaxRetries(int maxRetries) { 75 | settings.setMaxRetries(maxRetries); 76 | } 77 | 78 | public void setConnectTimeout(int connectTimeout) { 79 | settings.setConnectTimeout(connectTimeout); 80 | } 81 | 82 | public void setReadTimeout(int readTimeout) { 83 | settings.setReadTimeout(readTimeout); 84 | } 85 | 86 | public void setIncludeCallerData(boolean includeCallerData) { 87 | settings.setIncludeCallerData(includeCallerData); 88 | } 89 | 90 | public void setErrorsToStderr(boolean errorsToStderr) { 91 | settings.setErrorsToStderr(errorsToStderr); 92 | } 93 | 94 | public void setLogsToStderr(boolean logsToStderr) { 95 | settings.setLogsToStderr(logsToStderr); 96 | } 97 | 98 | public void setMaxQueueSize(int maxQueueSize) { 99 | settings.setMaxQueueSize(maxQueueSize); 100 | } 101 | 102 | public void setIndex(String index) { 103 | settings.setIndex(index); 104 | } 105 | 106 | public void setType(String type) { 107 | settings.setType(type); 108 | } 109 | 110 | public void setUrl(String url) throws MalformedURLException { 111 | settings.setUrl(new URL(url)); 112 | } 113 | 114 | public void setLoggerName(String logger) { 115 | settings.setLoggerName(logger); 116 | } 117 | 118 | public void setErrorLoggerName(String logger) { 119 | settings.setErrorLoggerName(logger); 120 | } 121 | 122 | public void setHeaders(HttpRequestHeaders httpRequestHeaders) { 123 | this.headers = httpRequestHeaders; 124 | } 125 | 126 | public void setRawJsonMessage(boolean rawJsonMessage) { 127 | settings.setRawJsonMessage(rawJsonMessage); 128 | } 129 | 130 | public void setIncludeMdc(boolean includeMdc) { 131 | settings.setIncludeMdc(includeMdc); 132 | } 133 | 134 | public void setAuthentication(Authentication auth) { 135 | settings.setAuthentication(auth); 136 | } 137 | 138 | public void setMaxMessageSize(int maxMessageSize) { 139 | settings.setMaxMessageSize(maxMessageSize); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/AbstractElasticsearchPublisher.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import ch.qos.logback.core.Context; 4 | import com.fasterxml.jackson.core.JsonFactory; 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.internetitem.logback.elasticsearch.config.ElasticsearchProperties; 7 | import com.internetitem.logback.elasticsearch.config.HttpRequestHeaders; 8 | import com.internetitem.logback.elasticsearch.config.Property; 9 | import com.internetitem.logback.elasticsearch.config.Settings; 10 | import com.internetitem.logback.elasticsearch.util.AbstractPropertyAndEncoder; 11 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 12 | import com.internetitem.logback.elasticsearch.writer.ElasticsearchWriter; 13 | import com.internetitem.logback.elasticsearch.writer.LoggerWriter; 14 | import com.internetitem.logback.elasticsearch.writer.StdErrWriter; 15 | 16 | import java.io.IOException; 17 | import java.text.DateFormat; 18 | import java.text.SimpleDateFormat; 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | 24 | public abstract class AbstractElasticsearchPublisher implements Runnable { 25 | 26 | private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1); 27 | private static final ThreadLocal DATE_FORMAT = new ThreadLocal () { 28 | @Override 29 | protected DateFormat initialValue() { 30 | return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); 31 | } 32 | }; 33 | 34 | public static final String THREAD_NAME_PREFIX = "es-writer-"; 35 | 36 | 37 | private volatile List events; 38 | private ElasticsearchOutputAggregator outputAggregator; 39 | private List> propertyList; 40 | 41 | private AbstractPropertyAndEncoder indexPattern; 42 | private JsonFactory jf; 43 | private JsonGenerator jsonGenerator; 44 | 45 | private ErrorReporter errorReporter; 46 | protected Settings settings; 47 | 48 | private final Object lock; 49 | 50 | private volatile boolean working; 51 | 52 | private final PropertySerializer propertySerializer; 53 | 54 | public AbstractElasticsearchPublisher(Context context, ErrorReporter errorReporter, Settings settings, ElasticsearchProperties properties, HttpRequestHeaders headers) throws IOException { 55 | this.errorReporter = errorReporter; 56 | this.events = new ArrayList(); 57 | this.lock = new Object(); 58 | this.settings = settings; 59 | 60 | this.outputAggregator = configureOutputAggregator(settings, errorReporter, headers); 61 | 62 | this.jf = new JsonFactory(); 63 | this.jf.setRootValueSeparator(null); 64 | this.jsonGenerator = jf.createGenerator(outputAggregator); 65 | 66 | this.indexPattern = buildPropertyAndEncoder(context, new Property("", settings.getIndex(), false)); 67 | this.propertyList = generatePropertyList(context, properties); 68 | 69 | this.propertySerializer = new PropertySerializer(); 70 | } 71 | 72 | private static ElasticsearchOutputAggregator configureOutputAggregator(Settings settings, ErrorReporter errorReporter, HttpRequestHeaders httpRequestHeaders) { 73 | ElasticsearchOutputAggregator spigot = new ElasticsearchOutputAggregator(settings, errorReporter); 74 | 75 | if (settings.isLogsToStderr()) { 76 | spigot.addWriter(new StdErrWriter()); 77 | } 78 | 79 | if (settings.getLoggerName() != null) { 80 | spigot.addWriter(new LoggerWriter(settings.getLoggerName())); 81 | } 82 | 83 | if (settings.getUrl() != null) { 84 | spigot.addWriter(new ElasticsearchWriter(errorReporter, settings, httpRequestHeaders)); 85 | } 86 | 87 | return spigot; 88 | } 89 | 90 | private List> generatePropertyList(Context context, ElasticsearchProperties properties) { 91 | List> list = new ArrayList>(); 92 | if (properties != null) { 93 | for (Property property : properties.getProperties()) { 94 | list.add(buildPropertyAndEncoder(context, property)); 95 | } 96 | } 97 | return list; 98 | } 99 | 100 | protected abstract AbstractPropertyAndEncoder buildPropertyAndEncoder(Context context, Property property); 101 | 102 | public void addEvent(T event) { 103 | if (!outputAggregator.hasOutputs()) { 104 | return; 105 | } 106 | 107 | synchronized (lock) { 108 | events.add(event); 109 | if (!working) { 110 | working = true; 111 | Thread thread = new Thread(this, THREAD_NAME_PREFIX + THREAD_COUNTER.getAndIncrement()); 112 | thread.start(); 113 | } 114 | } 115 | } 116 | 117 | public void run() { 118 | int currentTry = 1; 119 | int maxRetries = settings.getMaxRetries(); 120 | while (true) { 121 | try { 122 | Thread.sleep(settings.getSleepTime()); 123 | 124 | List eventsCopy = null; 125 | synchronized (lock) { 126 | if (!events.isEmpty()) { 127 | eventsCopy = events; 128 | events = new ArrayList(); 129 | currentTry = 1; 130 | } 131 | 132 | if (eventsCopy == null) { 133 | if (!outputAggregator.hasPendingData()) { 134 | // all done 135 | working = false; 136 | return; 137 | } else { 138 | // Nothing new, must be a retry 139 | if (currentTry > maxRetries) { 140 | // Oh well, better luck next time 141 | working = false; 142 | return; 143 | } 144 | } 145 | } 146 | } 147 | 148 | if (eventsCopy != null) { 149 | serializeEvents(jsonGenerator, eventsCopy, propertyList); 150 | } 151 | 152 | if (!outputAggregator.sendData()) { 153 | currentTry++; 154 | } 155 | } catch (Exception e) { 156 | errorReporter.logError("Internal error handling log data: " + e.getMessage(), e); 157 | currentTry++; 158 | } 159 | } 160 | } 161 | 162 | 163 | private void serializeEvents(JsonGenerator gen, List eventsCopy, List> propertyList) throws IOException { 164 | for (T event : eventsCopy) { 165 | serializeIndexString(gen, event); 166 | gen.writeRaw('\n'); 167 | serializeEvent(gen, event, propertyList); 168 | gen.writeRaw('\n'); 169 | } 170 | gen.flush(); 171 | } 172 | 173 | private void serializeIndexString(JsonGenerator gen, T event) throws IOException { 174 | gen.writeStartObject(); 175 | gen.writeObjectFieldStart("index"); 176 | gen.writeObjectField("_index", indexPattern.encode(event)); 177 | String type = settings.getType(); 178 | if (type != null) { 179 | gen.writeObjectField("_type", type); 180 | } 181 | gen.writeEndObject(); 182 | gen.writeEndObject(); 183 | } 184 | 185 | private void serializeEvent(JsonGenerator gen, T event, List> propertyList) throws IOException { 186 | gen.writeStartObject(); 187 | 188 | serializeCommonFields(gen, event); 189 | 190 | for (AbstractPropertyAndEncoder pae : propertyList) { 191 | propertySerializer.serializeProperty(gen, event, pae); 192 | } 193 | 194 | gen.writeEndObject(); 195 | } 196 | 197 | protected abstract void serializeCommonFields(JsonGenerator gen, T event) throws IOException; 198 | 199 | protected static String getTimestamp(long timestamp) { 200 | return DATE_FORMAT.get().format(new Date(timestamp)); 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/AccessElasticsearchPublisher.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import java.io.IOException; 4 | 5 | import ch.qos.logback.access.spi.IAccessEvent; 6 | import ch.qos.logback.core.Context; 7 | import com.fasterxml.jackson.core.JsonGenerator; 8 | import com.internetitem.logback.elasticsearch.config.ElasticsearchProperties; 9 | import com.internetitem.logback.elasticsearch.config.HttpRequestHeaders; 10 | import com.internetitem.logback.elasticsearch.config.Property; 11 | import com.internetitem.logback.elasticsearch.config.Settings; 12 | import com.internetitem.logback.elasticsearch.util.AbstractPropertyAndEncoder; 13 | import com.internetitem.logback.elasticsearch.util.AccessPropertyAndEncoder; 14 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 15 | 16 | public class AccessElasticsearchPublisher extends AbstractElasticsearchPublisher { 17 | 18 | public AccessElasticsearchPublisher(Context context, ErrorReporter errorReporter, Settings settings, ElasticsearchProperties properties, HttpRequestHeaders httpRequestHeaders) throws IOException { 19 | super(context, errorReporter, settings, properties, httpRequestHeaders); 20 | } 21 | 22 | @Override 23 | protected AbstractPropertyAndEncoder buildPropertyAndEncoder(Context context, Property property) { 24 | return new AccessPropertyAndEncoder(property, context); 25 | } 26 | 27 | @Override 28 | protected void serializeCommonFields(JsonGenerator gen, IAccessEvent event) throws IOException { 29 | gen.writeObjectField("@timestamp", getTimestamp(event.getTimeStamp())); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/ClassicElasticsearchPublisher.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | 6 | import ch.qos.logback.classic.spi.ILoggingEvent; 7 | import ch.qos.logback.core.Context; 8 | import com.fasterxml.jackson.core.JsonGenerator; 9 | import com.internetitem.logback.elasticsearch.config.ElasticsearchProperties; 10 | import com.internetitem.logback.elasticsearch.config.HttpRequestHeaders; 11 | import com.internetitem.logback.elasticsearch.config.Property; 12 | import com.internetitem.logback.elasticsearch.config.Settings; 13 | import com.internetitem.logback.elasticsearch.util.AbstractPropertyAndEncoder; 14 | import com.internetitem.logback.elasticsearch.util.ClassicPropertyAndEncoder; 15 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 16 | 17 | public class ClassicElasticsearchPublisher extends AbstractElasticsearchPublisher { 18 | 19 | public ClassicElasticsearchPublisher(Context context, ErrorReporter errorReporter, Settings settings, ElasticsearchProperties properties, HttpRequestHeaders headers) throws IOException { 20 | super(context, errorReporter, settings, properties, headers); 21 | } 22 | 23 | @Override 24 | protected AbstractPropertyAndEncoder buildPropertyAndEncoder(Context context, Property property) { 25 | return new ClassicPropertyAndEncoder(property, context); 26 | } 27 | 28 | @Override 29 | protected void serializeCommonFields(JsonGenerator gen, ILoggingEvent event) throws IOException { 30 | gen.writeObjectField("@timestamp", getTimestamp(event.getTimeStamp())); 31 | 32 | if (settings.isRawJsonMessage()) { 33 | gen.writeFieldName("message"); 34 | gen.writeRawValue(event.getFormattedMessage()); 35 | } else { 36 | String formattedMessage = event.getFormattedMessage(); 37 | if (settings.getMaxMessageSize() > 0 && formattedMessage.length() > settings.getMaxMessageSize()) { 38 | formattedMessage = formattedMessage.substring(0, settings.getMaxMessageSize()) + ".."; 39 | } 40 | gen.writeObjectField("message", formattedMessage); 41 | } 42 | 43 | if(settings.isIncludeMdc()) { 44 | for (Map.Entry entry : event.getMDCPropertyMap().entrySet()) { 45 | gen.writeObjectField(entry.getKey(), entry.getValue()); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/ElasticsearchAccessAppender.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import java.io.IOException; 4 | 5 | import ch.qos.logback.access.spi.IAccessEvent; 6 | import com.internetitem.logback.elasticsearch.config.Settings; 7 | 8 | public class ElasticsearchAccessAppender extends AbstractElasticsearchAppender { 9 | 10 | public ElasticsearchAccessAppender() { 11 | } 12 | 13 | public ElasticsearchAccessAppender(Settings settings) { 14 | super(settings); 15 | } 16 | 17 | @Override 18 | protected void appendInternal(IAccessEvent eventObject) { 19 | eventObject.prepareForDeferredProcessing(); 20 | publishEvent(eventObject); 21 | } 22 | 23 | protected AccessElasticsearchPublisher buildElasticsearchPublisher() throws IOException { 24 | return new AccessElasticsearchPublisher(getContext(), errorReporter, settings, elasticsearchProperties, headers); 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/ElasticsearchAppender.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import java.io.IOException; 4 | 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import com.internetitem.logback.elasticsearch.config.Settings; 7 | 8 | public class ElasticsearchAppender extends AbstractElasticsearchAppender { 9 | 10 | public ElasticsearchAppender() { 11 | } 12 | 13 | public ElasticsearchAppender(Settings settings) { 14 | super(settings); 15 | } 16 | 17 | @Override 18 | protected void appendInternal(ILoggingEvent eventObject) { 19 | 20 | String targetLogger = eventObject.getLoggerName(); 21 | 22 | String loggerName = settings.getLoggerName(); 23 | if (loggerName != null && loggerName.equals(targetLogger)) { 24 | return; 25 | } 26 | 27 | String errorLoggerName = settings.getErrorLoggerName(); 28 | if (errorLoggerName != null && errorLoggerName.equals(targetLogger)) { 29 | return; 30 | } 31 | 32 | eventObject.prepareForDeferredProcessing(); 33 | if (settings.isIncludeCallerData()) { 34 | eventObject.getCallerData(); 35 | } 36 | 37 | publishEvent(eventObject); 38 | } 39 | 40 | protected ClassicElasticsearchPublisher buildElasticsearchPublisher() throws IOException { 41 | return new ClassicElasticsearchPublisher(getContext(), errorReporter, settings, elasticsearchProperties, headers); 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/ElasticsearchOutputAggregator.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import com.internetitem.logback.elasticsearch.config.Settings; 4 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 5 | import com.internetitem.logback.elasticsearch.writer.SafeWriter; 6 | 7 | import java.io.IOException; 8 | import java.io.Writer; 9 | import java.util.ArrayList; 10 | import java.util.Date; 11 | import java.util.List; 12 | 13 | public class ElasticsearchOutputAggregator extends Writer { 14 | 15 | private Settings settings; 16 | private ErrorReporter errorReporter; 17 | private List writers; 18 | 19 | public ElasticsearchOutputAggregator(Settings settings, ErrorReporter errorReporter) { 20 | this.writers = new ArrayList(); 21 | this.settings = settings; 22 | this.errorReporter = errorReporter; 23 | } 24 | 25 | public void addWriter(SafeWriter writer) { 26 | writers.add(writer); 27 | } 28 | 29 | @Override 30 | public void write(char[] cbuf, int off, int len) throws IOException { 31 | for (SafeWriter writer : writers) { 32 | writer.write(cbuf, off, len); 33 | } 34 | } 35 | 36 | public boolean hasPendingData() { 37 | for (SafeWriter writer : writers) { 38 | if (writer.hasPendingData()) { 39 | return true; 40 | } 41 | } 42 | return false; 43 | } 44 | 45 | public boolean hasOutputs() { 46 | return !writers.isEmpty(); 47 | } 48 | 49 | public boolean sendData() { 50 | boolean success = true; 51 | for (SafeWriter writer : writers) { 52 | try { 53 | writer.sendData(); 54 | } catch (IOException e) { 55 | success = false; 56 | errorReporter.logWarning("Failed to send events to Elasticsearch: " + e.getMessage()); 57 | if (settings.isErrorsToStderr()) { 58 | System.err.println("[" + new Date().toString() + "] Failed to send events to Elasticsearch: " + e.getMessage()); 59 | } 60 | 61 | } 62 | } 63 | return success; 64 | } 65 | 66 | @Override 67 | public void flush() throws IOException { 68 | // No-op 69 | } 70 | 71 | @Override 72 | public void close() throws IOException { 73 | // No-op 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/PropertySerializer.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.internetitem.logback.elasticsearch.util.AbstractPropertyAndEncoder; 5 | 6 | import java.io.IOException; 7 | 8 | class PropertySerializer { 9 | void serializeProperty(JsonGenerator jsonGenerator, T event, AbstractPropertyAndEncoder propertyAndEncoder) throws IOException { 10 | String value = propertyAndEncoder.encode(event); 11 | if (propertyAndEncoder.allowEmpty() || (value != null && !value.isEmpty())) { 12 | switch (propertyAndEncoder.getType()) { 13 | case INT: 14 | serializeIntField(jsonGenerator, propertyAndEncoder, value); 15 | break; 16 | case FLOAT: 17 | serializeFloatField(jsonGenerator, propertyAndEncoder, value); 18 | break; 19 | case BOOLEAN: 20 | serializeBooleanField(jsonGenerator, propertyAndEncoder, value); 21 | break; 22 | default: 23 | serializeStringField(jsonGenerator, propertyAndEncoder, value); 24 | } 25 | } 26 | } 27 | 28 | private void serializeStringField(JsonGenerator jsonGenerator, AbstractPropertyAndEncoder propertyAndEncoder, String value) throws IOException { 29 | jsonGenerator.writeObjectField(propertyAndEncoder.getName(), value); 30 | } 31 | 32 | private void serializeIntField(JsonGenerator jsonGenerator, AbstractPropertyAndEncoder propertyAndEncoder, String value) throws IOException { 33 | try { 34 | jsonGenerator.writeNumberField(propertyAndEncoder.getName(), Integer.valueOf(value)); 35 | } catch (NumberFormatException e) { 36 | serializeStringField(jsonGenerator, propertyAndEncoder, value); 37 | } 38 | } 39 | 40 | private void serializeFloatField(JsonGenerator jsonGenerator, AbstractPropertyAndEncoder propertyAndEncoder, String value) throws IOException { 41 | try { 42 | jsonGenerator.writeNumberField(propertyAndEncoder.getName(), Float.valueOf(value)); 43 | } catch (NumberFormatException e) { 44 | serializeStringField(jsonGenerator, propertyAndEncoder, value); 45 | } 46 | } 47 | 48 | private void serializeBooleanField(JsonGenerator jsonGenerator, AbstractPropertyAndEncoder propertyAndEncoder, String value) throws IOException { 49 | if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { 50 | jsonGenerator.writeBooleanField(propertyAndEncoder.getName(), Boolean.valueOf(value)); 51 | } else { 52 | serializeStringField(jsonGenerator, propertyAndEncoder, value); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/AWSAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | import java.io.InputStream; 4 | import java.io.UnsupportedEncodingException; 5 | import java.net.HttpURLConnection; 6 | import java.net.URI; 7 | import java.net.URISyntaxException; 8 | import java.net.URL; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import com.amazonaws.ReadLimitInfo; 15 | import com.amazonaws.SignableRequest; 16 | import com.amazonaws.auth.AWS4Signer; 17 | import com.amazonaws.auth.AWSCredentials; 18 | import com.amazonaws.auth.AWSCredentialsProvider; 19 | import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; 20 | import com.amazonaws.http.HttpMethodName; 21 | import com.amazonaws.regions.DefaultAwsRegionProviderChain; 22 | import com.amazonaws.util.StringInputStream; 23 | 24 | /** 25 | * This class implements Amazon AWS v4 Signature signing for ElasticSearch. 26 | * 27 | * @author blagerweij 28 | */ 29 | public class AWSAuthentication implements Authentication { 30 | 31 | private final AWS4Signer signer; 32 | private final AWSCredentials credentials; 33 | 34 | public AWSAuthentication() { 35 | signer = new AWS4Signer(false); 36 | signer.setServiceName("es"); 37 | signer.setRegionName(new DefaultAwsRegionProviderChain().getRegion()); 38 | AWSCredentialsProvider credsProvider = new DefaultAWSCredentialsProviderChain(); 39 | credentials = credsProvider.getCredentials(); 40 | } 41 | 42 | @Override 43 | public void addAuth(HttpURLConnection urlConnection, String body) { 44 | 45 | signer.sign(new URLConnectionSignableRequest(urlConnection, body), credentials); 46 | } 47 | 48 | /** 49 | * Wrapper for signing a HttpURLConnection 50 | */ 51 | private static class URLConnectionSignableRequest implements SignableRequest { 52 | 53 | private final HttpURLConnection urlConnection; 54 | private final String body; 55 | private final Map headers = new HashMap<>(); 56 | 57 | public URLConnectionSignableRequest(HttpURLConnection urlConnection, String body) { 58 | this.urlConnection = urlConnection; 59 | this.body = body; 60 | addHeader("User-Agent","ElasticSearchWriter/1.0"); 61 | addHeader("Accept","*/*"); 62 | addHeader("Content-Type","application/json"); 63 | addHeader("Content-Length",String.valueOf(body.length())); 64 | } 65 | 66 | @Override 67 | public void addHeader(String name, String value) { 68 | this.urlConnection.addRequestProperty(name, value); 69 | headers.put(name,value); 70 | } 71 | 72 | @Override 73 | public Map getHeaders() { 74 | return headers; 75 | } 76 | 77 | @Override 78 | public String getResourcePath() { 79 | return urlConnection.getURL().getPath(); 80 | } 81 | 82 | @Override 83 | public void addParameter(String name, String value) { 84 | 85 | } 86 | 87 | @Override 88 | public Map> getParameters() { 89 | return Collections.emptyMap(); 90 | } 91 | 92 | @Override 93 | public URI getEndpoint() { 94 | try { 95 | URL u = urlConnection.getURL(); 96 | return new URI(u.getProtocol(),null, u.getHost(), u.getPort(), null, null, null); 97 | } catch (URISyntaxException e) { 98 | throw new RuntimeException(e); 99 | } 100 | } 101 | 102 | @Override 103 | public HttpMethodName getHttpMethod() { 104 | return HttpMethodName.fromValue(urlConnection.getRequestMethod()); 105 | } 106 | 107 | @Override 108 | public int getTimeOffset() { 109 | return 0; 110 | } 111 | 112 | @Override 113 | public InputStream getContent() { 114 | try { 115 | return new StringInputStream(body); 116 | } catch (UnsupportedEncodingException e) { 117 | throw new RuntimeException(e); 118 | } 119 | 120 | } 121 | 122 | @Override 123 | public void setContent(InputStream content) { 124 | } 125 | 126 | @Override 127 | public InputStream getContentUnwrapped() { 128 | return getContent(); 129 | } 130 | 131 | @Override 132 | public ReadLimitInfo getReadLimitInfo() { 133 | return null; 134 | } 135 | 136 | @Override 137 | public Object getOriginalRequestObject() { 138 | return null; 139 | } 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/Authentication.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | import java.net.HttpURLConnection; 4 | 5 | public interface Authentication { 6 | /** 7 | * Modify the given urlConnection for whatever authentication scheme is used. 8 | * 9 | * @param urlConnection the connection to the server 10 | * @param body the message being sent 11 | */ 12 | void addAuth(HttpURLConnection urlConnection, String body); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/BasicAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | import com.internetitem.logback.elasticsearch.util.Base64; 4 | 5 | import java.net.HttpURLConnection; 6 | 7 | public class BasicAuthentication implements Authentication { 8 | public void addAuth(HttpURLConnection urlConnection, String body) { 9 | String userInfo = urlConnection.getURL().getUserInfo(); 10 | if (userInfo != null) { 11 | String basicAuth = "Basic " + Base64.encode(userInfo.getBytes()); 12 | urlConnection.setRequestProperty("Authorization", basicAuth); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/ElasticsearchProperties.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class ElasticsearchProperties { 7 | 8 | private List properties; 9 | 10 | public ElasticsearchProperties() { 11 | this.properties = new ArrayList(); 12 | } 13 | 14 | public List getProperties() { 15 | return properties; 16 | } 17 | 18 | public void addProperty(Property property) { 19 | properties.add(property); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/HttpRequestHeader.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | /** 4 | * A key value pair for the http header. 5 | */ 6 | public class HttpRequestHeader { 7 | 8 | private String name; 9 | private String value; 10 | 11 | public String getName() { 12 | return name; 13 | } 14 | 15 | public void setName(String name) { 16 | this.name = name; 17 | } 18 | 19 | public String getValue() { 20 | return value; 21 | } 22 | 23 | public void setValue(String value) { 24 | this.value = value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/HttpRequestHeaders.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | /** 7 | * A container for the headers which will be sent to elasticsearch. 8 | */ 9 | public class HttpRequestHeaders { 10 | 11 | private List headers = new LinkedList(); 12 | 13 | public List getHeaders() { 14 | return headers; 15 | } 16 | 17 | public void addHeader(HttpRequestHeader header) { 18 | this.headers.add(header); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/Property.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | public class Property { 4 | private String name; 5 | private String value; 6 | private boolean allowEmpty; 7 | private Type type = Type.STRING; 8 | 9 | public enum Type { 10 | STRING, INT, FLOAT, BOOLEAN 11 | } 12 | 13 | public Property() { 14 | } 15 | 16 | public Property(String name, String value, boolean allowEmpty) { 17 | this.name = name; 18 | this.value = value; 19 | this.allowEmpty = allowEmpty; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public String getValue() { 31 | return value; 32 | } 33 | 34 | public void setValue(String value) { 35 | this.value = value; 36 | } 37 | 38 | public boolean isAllowEmpty() { 39 | return allowEmpty; 40 | } 41 | 42 | public void setAllowEmpty(boolean allowEmpty) { 43 | this.allowEmpty = allowEmpty; 44 | } 45 | 46 | public Type getType() { 47 | return type; 48 | } 49 | 50 | public void setType(String type) { 51 | try { 52 | this.type = Enum.valueOf(Type.class, type.toUpperCase()); 53 | } catch (IllegalArgumentException e) { 54 | this.type = Type.STRING; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/config/Settings.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.config; 2 | 3 | import java.net.URL; 4 | 5 | public class Settings { 6 | 7 | private String index; 8 | private String type; 9 | private URL url; 10 | 11 | private String loggerName; 12 | private String errorLoggerName; 13 | 14 | private int sleepTime = 250; 15 | private int maxRetries = 3; 16 | private int connectTimeout = 30000; 17 | private int readTimeout = 30000; 18 | private boolean logsToStderr; 19 | private boolean errorsToStderr; 20 | private boolean includeCallerData; 21 | private boolean includeMdc; 22 | private boolean rawJsonMessage; 23 | private int maxQueueSize = 100 * 1024 * 1024; 24 | private Authentication authentication; 25 | private int maxMessageSize = -1; 26 | 27 | public String getIndex() { 28 | return index; 29 | } 30 | 31 | public void setIndex(String index) { 32 | this.index = index; 33 | } 34 | 35 | public String getType() { 36 | return type; 37 | } 38 | 39 | public void setType(String type) { 40 | this.type = type; 41 | } 42 | 43 | public int getSleepTime() { 44 | return sleepTime; 45 | } 46 | 47 | public void setSleepTime(int sleepTime) { 48 | if (sleepTime < 100) { 49 | sleepTime = 100; 50 | } 51 | this.sleepTime = sleepTime; 52 | } 53 | 54 | public int getMaxRetries() { 55 | return maxRetries; 56 | } 57 | 58 | public void setMaxRetries(int maxRetries) { 59 | this.maxRetries = maxRetries; 60 | } 61 | 62 | public int getConnectTimeout() { 63 | return connectTimeout; 64 | } 65 | 66 | public void setConnectTimeout(int connectTimeout) { 67 | this.connectTimeout = connectTimeout; 68 | } 69 | 70 | public int getReadTimeout() { 71 | return readTimeout; 72 | } 73 | 74 | public void setReadTimeout(int readTimeout) { 75 | this.readTimeout = readTimeout; 76 | } 77 | 78 | public boolean isLogsToStderr() { 79 | return logsToStderr; 80 | } 81 | 82 | public void setLogsToStderr(boolean logsToStderr) { 83 | this.logsToStderr = logsToStderr; 84 | } 85 | 86 | public boolean isErrorsToStderr() { 87 | return errorsToStderr; 88 | } 89 | 90 | public void setErrorsToStderr(boolean errorsToStderr) { 91 | this.errorsToStderr = errorsToStderr; 92 | } 93 | 94 | public boolean isIncludeCallerData() { 95 | return includeCallerData; 96 | } 97 | 98 | public void setIncludeCallerData(boolean includeCallerData) { 99 | this.includeCallerData = includeCallerData; 100 | } 101 | 102 | public int getMaxQueueSize() { 103 | return maxQueueSize; 104 | } 105 | 106 | public void setMaxQueueSize(int maxQueueSize) { 107 | this.maxQueueSize = maxQueueSize; 108 | } 109 | 110 | public String getLoggerName() { 111 | return loggerName; 112 | } 113 | 114 | public void setLoggerName(String loggerName) { 115 | this.loggerName = loggerName; 116 | } 117 | 118 | public URL getUrl() { 119 | return url; 120 | } 121 | 122 | public void setUrl(URL url) { 123 | this.url = url; 124 | } 125 | 126 | public String getErrorLoggerName() { 127 | return errorLoggerName; 128 | } 129 | 130 | public void setErrorLoggerName(String errorLoggerName) { 131 | this.errorLoggerName = errorLoggerName; 132 | } 133 | 134 | public boolean isRawJsonMessage() { 135 | return rawJsonMessage; 136 | } 137 | 138 | public void setRawJsonMessage(boolean rawJsonMessage) { 139 | this.rawJsonMessage = rawJsonMessage; 140 | } 141 | 142 | public Authentication getAuthentication() { 143 | return authentication; 144 | } 145 | 146 | public void setAuthentication(Authentication authentication) { 147 | this.authentication = authentication; 148 | } 149 | 150 | public boolean isIncludeMdc() { 151 | return includeMdc; 152 | } 153 | 154 | public void setIncludeMdc(boolean includeMdc) { 155 | this.includeMdc = includeMdc; 156 | } 157 | 158 | public int getMaxMessageSize() { 159 | return maxMessageSize; 160 | } 161 | 162 | public void setMaxMessageSize(int maxMessageSize) { 163 | this.maxMessageSize = maxMessageSize; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/util/AbstractPropertyAndEncoder.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.util; 2 | 3 | import ch.qos.logback.core.Context; 4 | import ch.qos.logback.core.pattern.PatternLayoutBase; 5 | import com.internetitem.logback.elasticsearch.config.Property; 6 | 7 | public abstract class AbstractPropertyAndEncoder { 8 | private Property property; 9 | private PatternLayoutBase layout; 10 | 11 | public AbstractPropertyAndEncoder(Property property, Context context) { 12 | this.property = property; 13 | 14 | this.layout = getLayout(); 15 | this.layout.setContext(context); 16 | this.layout.setPattern(property.getValue()); 17 | this.layout.setPostCompileProcessor(null); 18 | this.layout.start(); 19 | } 20 | 21 | protected abstract PatternLayoutBase getLayout(); 22 | 23 | public String encode(T event) { 24 | return layout.doLayout(event); 25 | } 26 | 27 | public String getName() { 28 | return property.getName(); 29 | } 30 | 31 | public boolean allowEmpty() { 32 | return property.isAllowEmpty(); 33 | } 34 | 35 | public Property.Type getType() { 36 | return property.getType(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/util/AccessPropertyAndEncoder.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.util; 2 | 3 | import ch.qos.logback.access.spi.IAccessEvent; 4 | import ch.qos.logback.access.PatternLayout; 5 | import ch.qos.logback.core.Context; 6 | import ch.qos.logback.core.pattern.PatternLayoutBase; 7 | import com.internetitem.logback.elasticsearch.config.Property; 8 | 9 | public class AccessPropertyAndEncoder extends AbstractPropertyAndEncoder { 10 | 11 | public AccessPropertyAndEncoder(Property property, Context context) { 12 | super(property, context); 13 | } 14 | 15 | @Override 16 | protected PatternLayoutBase getLayout() { 17 | return new PatternLayout(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/util/Base64.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.util; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | 5 | public class Base64 6 | { 7 | public static String encode(byte[] data) 8 | { 9 | char[] tbl = { 10 | 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 11 | 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 12 | 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 13 | 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; 14 | 15 | StringBuilder buffer = new StringBuilder(); 16 | int pad = 0; 17 | for (int i = 0; i < data.length; i += 3) { 18 | 19 | int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF; 20 | if (i + 1 < data.length) { 21 | b |= (data[i+1] & 0xFF) << 8; 22 | } else { 23 | pad++; 24 | } 25 | if (i + 2 < data.length) { 26 | b |= (data[i+2] & 0xFF); 27 | } else { 28 | pad++; 29 | } 30 | 31 | for (int j = 0; j < 4 - pad; j++) { 32 | int c = (b & 0xFC0000) >> 18; 33 | buffer.append(tbl[c]); 34 | b <<= 6; 35 | } 36 | } 37 | for (int j = 0; j < pad; j++) { 38 | buffer.append("="); 39 | } 40 | 41 | return buffer.toString(); 42 | } 43 | 44 | public static byte[] decode(String data) 45 | { 46 | int[] tbl = { 47 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 48 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 49 | -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 50 | 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 51 | 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 52 | 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 53 | 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 54 | 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 55 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 56 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 57 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 58 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 59 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 60 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 61 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; 62 | byte[] bytes = data.getBytes(); 63 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 64 | for (int i = 0; i < bytes.length; ) { 65 | int b = 0; 66 | if (tbl[bytes[i]] != -1) { 67 | b = (tbl[bytes[i]] & 0xFF) << 18; 68 | } 69 | // skip unknown characters 70 | else { 71 | i++; 72 | continue; 73 | } 74 | 75 | int num = 0; 76 | if (i + 1 < bytes.length && tbl[bytes[i+1]] != -1) { 77 | b = b | ((tbl[bytes[i+1]] & 0xFF) << 12); 78 | num++; 79 | } 80 | if (i + 2 < bytes.length && tbl[bytes[i+2]] != -1) { 81 | b = b | ((tbl[bytes[i+2]] & 0xFF) << 6); 82 | num++; 83 | } 84 | if (i + 3 < bytes.length && tbl[bytes[i+3]] != -1) { 85 | b = b | (tbl[bytes[i+3]] & 0xFF); 86 | num++; 87 | } 88 | 89 | while (num > 0) { 90 | int c = (b & 0xFF0000) >> 16; 91 | buffer.write((char)c); 92 | b <<= 8; 93 | num--; 94 | } 95 | i += 4; 96 | } 97 | return buffer.toByteArray(); 98 | } 99 | } -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/util/ClassicPropertyAndEncoder.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.util; 2 | 3 | import ch.qos.logback.classic.PatternLayout; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import ch.qos.logback.core.Context; 6 | import ch.qos.logback.core.pattern.PatternLayoutBase; 7 | import com.internetitem.logback.elasticsearch.config.Property; 8 | 9 | public class ClassicPropertyAndEncoder extends AbstractPropertyAndEncoder { 10 | 11 | public ClassicPropertyAndEncoder(Property property, Context context) { 12 | super(property, context); 13 | } 14 | 15 | @Override 16 | protected PatternLayoutBase getLayout() { 17 | return new PatternLayout(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/util/ErrorReporter.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.util; 2 | 3 | import ch.qos.logback.core.Context; 4 | import ch.qos.logback.core.spi.ContextAwareBase; 5 | import com.internetitem.logback.elasticsearch.config.Settings; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public class ErrorReporter extends ContextAwareBase { 9 | 10 | private Settings settings; 11 | 12 | public ErrorReporter(Settings settings, Context context) { 13 | setContext(context); 14 | this.settings = settings; 15 | } 16 | 17 | public void logError(String message, Throwable e) { 18 | String loggerName = settings.getErrorLoggerName(); 19 | if (loggerName != null) { 20 | LoggerFactory.getLogger(loggerName).error(message, e); 21 | } 22 | addError(message, e); 23 | } 24 | 25 | public void logWarning(String message) { 26 | String loggerName = settings.getErrorLoggerName(); 27 | if (loggerName != null) { 28 | LoggerFactory.getLogger(loggerName).warn(message); 29 | } 30 | addWarn(message); 31 | } 32 | 33 | public void logInfo(String message) { 34 | String loggerName = settings.getErrorLoggerName(); 35 | if (loggerName != null) { 36 | LoggerFactory.getLogger(loggerName).info(message); 37 | } 38 | addInfo(message); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/writer/ElasticsearchWriter.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.writer; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.InputStreamReader; 6 | import java.io.OutputStreamWriter; 7 | import java.io.Writer; 8 | import java.net.HttpURLConnection; 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | 12 | import com.internetitem.logback.elasticsearch.config.HttpRequestHeader; 13 | import com.internetitem.logback.elasticsearch.config.HttpRequestHeaders; 14 | import com.internetitem.logback.elasticsearch.config.Settings; 15 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 16 | 17 | public class ElasticsearchWriter implements SafeWriter { 18 | 19 | private StringBuilder sendBuffer; 20 | 21 | private ErrorReporter errorReporter; 22 | private Settings settings; 23 | private Collection headerList; 24 | 25 | private boolean bufferExceeded; 26 | 27 | public ElasticsearchWriter(ErrorReporter errorReporter, Settings settings, HttpRequestHeaders headers) { 28 | this.errorReporter = errorReporter; 29 | this.settings = settings; 30 | this.headerList = headers != null && headers.getHeaders() != null 31 | ? headers.getHeaders() 32 | : Collections.emptyList(); 33 | 34 | this.sendBuffer = new StringBuilder(); 35 | } 36 | 37 | public void write(char[] cbuf, int off, int len) { 38 | if (bufferExceeded) { 39 | return; 40 | } 41 | 42 | sendBuffer.append(cbuf, off, len); 43 | 44 | if (sendBuffer.length() >= settings.getMaxQueueSize()) { 45 | errorReporter.logWarning("Send queue maximum size exceeded - log messages will be lost until the buffer is cleared"); 46 | bufferExceeded = true; 47 | } 48 | } 49 | 50 | public void sendData() throws IOException { 51 | if (sendBuffer.length() <= 0) { 52 | return; 53 | } 54 | 55 | HttpURLConnection urlConnection = (HttpURLConnection)(settings.getUrl().openConnection()); 56 | try { 57 | urlConnection.setDoInput(true); 58 | urlConnection.setDoOutput(true); 59 | urlConnection.setReadTimeout(settings.getReadTimeout()); 60 | urlConnection.setConnectTimeout(settings.getConnectTimeout()); 61 | urlConnection.setRequestMethod("POST"); 62 | 63 | String body = sendBuffer.toString(); 64 | 65 | if (!headerList.isEmpty()) { 66 | for(HttpRequestHeader header: headerList) { 67 | urlConnection.setRequestProperty(header.getName(), header.getValue()); 68 | } 69 | } 70 | 71 | if (settings.getAuthentication() != null) { 72 | settings.getAuthentication().addAuth(urlConnection, body); 73 | } 74 | 75 | Writer writer = new OutputStreamWriter(urlConnection.getOutputStream(), "UTF-8"); 76 | writer.write(body); 77 | writer.flush(); 78 | writer.close(); 79 | 80 | int rc = urlConnection.getResponseCode(); 81 | if (rc != 200) { 82 | String data = slurpErrors(urlConnection); 83 | throw new IOException("Got response code [" + rc + "] from server with data " + data); 84 | } 85 | } finally { 86 | urlConnection.disconnect(); 87 | } 88 | 89 | sendBuffer.setLength(0); 90 | if (bufferExceeded) { 91 | errorReporter.logInfo("Send queue cleared - log messages will no longer be lost"); 92 | bufferExceeded = false; 93 | } 94 | } 95 | 96 | public boolean hasPendingData() { 97 | return sendBuffer.length() != 0; 98 | } 99 | 100 | private static String slurpErrors(HttpURLConnection urlConnection) { 101 | try { 102 | InputStream stream = urlConnection.getErrorStream(); 103 | if (stream == null) { 104 | return ""; 105 | } 106 | 107 | StringBuilder builder = new StringBuilder(); 108 | InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); 109 | char[] buf = new char[2048]; 110 | int numRead; 111 | while ((numRead = reader.read(buf)) > 0) { 112 | builder.append(buf, 0, numRead); 113 | } 114 | return builder.toString(); 115 | } catch (Exception e) { 116 | return ""; 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/writer/LoggerWriter.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.writer; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class LoggerWriter implements SafeWriter { 7 | 8 | private String loggerName; 9 | 10 | private Logger logger; 11 | 12 | public LoggerWriter(String loggerName) { 13 | this.loggerName = loggerName; 14 | } 15 | 16 | public void write(char[] cbuf, int off, int len) { 17 | if (logger == null) { 18 | logger = LoggerFactory.getLogger(loggerName); 19 | } 20 | logger.info(new String(cbuf, 0, len)); 21 | } 22 | 23 | public void sendData() { 24 | // No-op 25 | } 26 | 27 | public boolean hasPendingData() { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/writer/SafeWriter.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.writer; 2 | 3 | import java.io.IOException; 4 | 5 | public interface SafeWriter { 6 | 7 | void write(char[] cbuf, int off, int len); 8 | 9 | void sendData() throws IOException; 10 | 11 | boolean hasPendingData(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/internetitem/logback/elasticsearch/writer/StdErrWriter.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch.writer; 2 | 3 | public class StdErrWriter implements SafeWriter { 4 | 5 | public void write(char[] cbuf, int off, int len) { 6 | System.err.println(new String(cbuf, 0, len)); 7 | } 8 | 9 | public void sendData() { 10 | // No-op 11 | } 12 | 13 | public boolean hasPendingData() { 14 | return false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/internetitem/logback/elasticsearch/ElasticsearchAppenderTest.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import ch.qos.logback.core.Context; 6 | import com.internetitem.logback.elasticsearch.config.ElasticsearchProperties; 7 | import com.internetitem.logback.elasticsearch.config.Settings; 8 | import com.internetitem.logback.elasticsearch.util.ErrorReporter; 9 | import org.hamcrest.core.IsInstanceOf; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | 16 | import java.io.IOException; 17 | import java.net.MalformedURLException; 18 | import java.net.URL; 19 | 20 | import static org.hamcrest.core.Is.is; 21 | import static org.junit.Assert.assertThat; 22 | import static org.mockito.BDDMockito.given; 23 | import static org.mockito.Mockito.*; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class ElasticsearchAppenderTest { 27 | 28 | 29 | @Mock 30 | private ClassicElasticsearchPublisher elasticsearchPublisher; 31 | @Mock 32 | private ErrorReporter errorReporter; 33 | @Mock 34 | private Settings settings; 35 | @Mock 36 | private ElasticsearchProperties elasticsearchProperties; 37 | @Mock 38 | private Context mockedContext; 39 | 40 | private boolean publisherSet = false; 41 | private boolean errorReporterSet = false; 42 | private AbstractElasticsearchAppender appender; 43 | 44 | @Before 45 | public void setUp() { 46 | 47 | appender = new ElasticsearchAppender() { 48 | @Override 49 | protected ClassicElasticsearchPublisher buildElasticsearchPublisher() throws IOException { 50 | publisherSet = true; 51 | return elasticsearchPublisher; 52 | } 53 | 54 | @Override 55 | protected ErrorReporter getErrorReporter() { 56 | errorReporterSet = true; 57 | return errorReporter; 58 | } 59 | }; 60 | } 61 | 62 | @Test 63 | public void should_set_the_collaborators_when_started() { 64 | appender.start(); 65 | 66 | 67 | assertThat(publisherSet, is(true)); 68 | assertThat(errorReporterSet, is(true)); 69 | } 70 | 71 | @Test 72 | public void should_throw_error_when_publisher_setup_fails_during_startup() { 73 | ElasticsearchAppender appender = new ElasticsearchAppender() { 74 | @Override 75 | protected ClassicElasticsearchPublisher buildElasticsearchPublisher() throws IOException { 76 | throw new IOException("Failed to start Publisher"); 77 | } 78 | }; 79 | 80 | try { 81 | appender.start(); 82 | } catch (Exception e) { 83 | assertThat(e, IsInstanceOf.instanceOf(RuntimeException.class)); 84 | assertThat(e.getMessage(), is("java.io.IOException: Failed to start Publisher")); 85 | } 86 | 87 | 88 | } 89 | 90 | @Test 91 | public void should_not_publish_events_when_logger_set() { 92 | String loggerName = "elastic-debug-log"; 93 | ILoggingEvent eventToLog = mock(ILoggingEvent.class); 94 | given(eventToLog.getLoggerName()).willReturn(loggerName); 95 | 96 | 97 | appender.setLoggerName(loggerName); 98 | appender.start(); 99 | 100 | 101 | appender.append(eventToLog); 102 | 103 | verifyZeroInteractions(elasticsearchPublisher); 104 | } 105 | 106 | 107 | @Test 108 | public void should_not_publish_events_when_errorlogger_set() { 109 | String errorLoggerName = "elastic-error-log"; 110 | ILoggingEvent eventToLog = mock(ILoggingEvent.class); 111 | given(eventToLog.getLoggerName()).willReturn(errorLoggerName); 112 | 113 | 114 | appender.setErrorLoggerName(errorLoggerName); 115 | appender.start(); 116 | 117 | 118 | appender.append(eventToLog); 119 | 120 | verifyZeroInteractions(elasticsearchPublisher); 121 | } 122 | 123 | 124 | @Test 125 | public void should_publish_events_when_loggername_is_null() { 126 | ILoggingEvent eventToPublish = mock(ILoggingEvent.class); 127 | given(eventToPublish.getLoggerName()).willReturn(null); 128 | String errorLoggerName = "es-error"; 129 | 130 | appender.setErrorLoggerName(errorLoggerName); 131 | appender.start(); 132 | 133 | 134 | appender.append(eventToPublish); 135 | 136 | verify(elasticsearchPublisher, times(1)).addEvent(eventToPublish); 137 | } 138 | 139 | 140 | @Test 141 | public void should_publish_events_when_loggername_is_different_from_the_elasticsearch_loggers() { 142 | ILoggingEvent eventToPublish = mock(ILoggingEvent.class); 143 | String differentLoggerName = "different-logger"; 144 | String errorLoggerName = "es-errors"; 145 | given(eventToPublish.getLoggerName()).willReturn(differentLoggerName); 146 | 147 | 148 | appender.setErrorLoggerName(errorLoggerName); 149 | appender.start(); 150 | 151 | 152 | appender.append(eventToPublish); 153 | 154 | verify(elasticsearchPublisher, times(1)).addEvent(eventToPublish); 155 | } 156 | 157 | @Test 158 | public void should_create_error_reporter_with_same_context() { 159 | ElasticsearchAppender appender = new ElasticsearchAppender(){ 160 | @Override 161 | public Context getContext() { 162 | return mockedContext; 163 | } 164 | }; 165 | 166 | ErrorReporter errorReporter = appender.getErrorReporter(); 167 | 168 | assertThat(errorReporter.getContext(), is(mockedContext)); 169 | } 170 | 171 | 172 | @Test 173 | public void should_delegate_setters_to_settings() throws MalformedURLException { 174 | ElasticsearchAppender appender = new ElasticsearchAppender(settings); 175 | boolean includeCallerData = false; 176 | boolean errorsToStderr = false; 177 | boolean rawJsonMessage = false; 178 | boolean includeMdc = true; 179 | String index = "app-logs"; 180 | String type = "appenderType"; 181 | int maxQueueSize = 10; 182 | String logger = "es-logger"; 183 | String url = "http://myelasticsearch.mycompany.com"; 184 | String errorLogger = "es-error-logger"; 185 | int maxRetries = 10000; 186 | int aSleepTime = 10000; 187 | int readTimeout = 10000; 188 | int connectTimeout = 5000; 189 | 190 | appender.setIncludeCallerData(includeCallerData); 191 | appender.setSleepTime(aSleepTime); 192 | appender.setReadTimeout(readTimeout); 193 | appender.setErrorsToStderr(errorsToStderr); 194 | appender.setLogsToStderr(errorsToStderr); 195 | appender.setMaxQueueSize(maxQueueSize); 196 | appender.setIndex(index); 197 | appender.setType(type); 198 | appender.setUrl(url); 199 | appender.setLoggerName(logger); 200 | appender.setErrorLoggerName(errorLogger); 201 | appender.setMaxRetries(maxRetries); 202 | appender.setConnectTimeout(connectTimeout); 203 | appender.setRawJsonMessage(rawJsonMessage); 204 | appender.setIncludeMdc(includeMdc); 205 | 206 | verify(settings, times(1)).setReadTimeout(readTimeout); 207 | verify(settings, times(1)).setSleepTime(aSleepTime); 208 | verify(settings, times(1)).setIncludeCallerData(includeCallerData); 209 | verify(settings, times(1)).setErrorsToStderr(errorsToStderr); 210 | verify(settings, times(1)).setLogsToStderr(errorsToStderr); 211 | verify(settings, times(1)).setMaxQueueSize(maxQueueSize); 212 | verify(settings, times(1)).setIndex(index); 213 | verify(settings, times(1)).setType(type); 214 | verify(settings, times(1)).setUrl(new URL(url)); 215 | verify(settings, times(1)).setLoggerName(logger); 216 | verify(settings, times(1)).setErrorLoggerName(errorLogger); 217 | verify(settings, times(1)).setMaxRetries(maxRetries); 218 | verify(settings, times(1)).setConnectTimeout(connectTimeout); 219 | verify(settings, times(1)).setRawJsonMessage(rawJsonMessage); 220 | verify(settings, times(1)).setIncludeMdc(includeMdc); 221 | } 222 | 223 | 224 | } -------------------------------------------------------------------------------- /src/test/java/com/internetitem/logback/elasticsearch/PropertySerializerTest.java: -------------------------------------------------------------------------------- 1 | package com.internetitem.logback.elasticsearch; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.Context; 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.internetitem.logback.elasticsearch.config.Property; 7 | import com.internetitem.logback.elasticsearch.util.ClassicPropertyAndEncoder; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | 13 | import static org.hamcrest.CoreMatchers.is; 14 | import static org.junit.Assert.assertThat; 15 | import static org.mockito.Mockito.verify; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class PropertySerializerTest { 19 | @Mock 20 | private Context context; 21 | 22 | @Mock 23 | private JsonGenerator jsonGenerator; 24 | 25 | @Mock 26 | private ILoggingEvent loggingEvent; 27 | 28 | private PropertySerializer propertySerializer = new PropertySerializer<>(); 29 | 30 | @Test 31 | public void should_default_to_string_type() throws Exception { 32 | // given 33 | Property property = new Property(); 34 | property.setValue("propertyValue"); 35 | 36 | // when 37 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 38 | 39 | // then 40 | assertThat(property.getType(), is(Property.Type.STRING)); 41 | verify(jsonGenerator).writeObject("propertyValue"); 42 | } 43 | 44 | @Test 45 | public void should_serialize_int_as_number() throws Exception { 46 | // given 47 | Property property = new Property(); 48 | property.setValue("123"); 49 | property.setType("int"); 50 | 51 | // when 52 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 53 | 54 | // then 55 | verify(jsonGenerator).writeNumber(123); 56 | } 57 | 58 | @Test 59 | public void should_serialize_object_when_invalid_int() throws Exception { 60 | // given 61 | Property property = new Property(); 62 | property.setValue("A123Z"); 63 | property.setType("int"); 64 | 65 | // when 66 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 67 | 68 | // then 69 | verify(jsonGenerator).writeObject("A123Z"); 70 | } 71 | 72 | @Test 73 | public void should_serialize_float_as_number() throws Exception { 74 | // given 75 | Property property = new Property(); 76 | property.setValue("12.30"); 77 | property.setType("float"); 78 | 79 | // when 80 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 81 | 82 | // then 83 | verify(jsonGenerator).writeNumber(12.30f); 84 | } 85 | 86 | @Test 87 | public void should_serialize_object_when_invalid_float() throws Exception { 88 | // given 89 | Property property = new Property(); 90 | property.setValue("A12.30Z"); 91 | property.setType("float"); 92 | 93 | // when 94 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 95 | 96 | // then 97 | verify(jsonGenerator).writeObject("A12.30Z"); 98 | } 99 | 100 | @Test 101 | public void should_serialize_true_as_boolean() throws Exception { 102 | // given 103 | Property property = new Property(); 104 | property.setValue("true"); 105 | property.setType("boolean"); 106 | 107 | // when 108 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 109 | 110 | // then 111 | verify(jsonGenerator).writeBoolean(true); 112 | } 113 | 114 | @Test 115 | public void should_serialize_object_when_invalid_boolean() throws Exception { 116 | // given 117 | Property property = new Property(); 118 | property.setValue("AtrueZ"); 119 | property.setType("boolean"); 120 | 121 | // when 122 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 123 | 124 | // then 125 | verify(jsonGenerator).writeObject("AtrueZ"); 126 | } 127 | 128 | @Test 129 | public void should_serialize_object_when_invalid_type() throws Exception { 130 | // given 131 | Property property = new Property(); 132 | property.setValue("value"); 133 | property.setType("invalidType"); 134 | 135 | // when 136 | propertySerializer.serializeProperty(jsonGenerator, loggingEvent, new ClassicPropertyAndEncoder(property, context)); 137 | 138 | // then 139 | verify(jsonGenerator).writeObject("value"); 140 | } 141 | } --------------------------------------------------------------------------------