1 /**
2  * Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  *      http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  * See the License for the specific language governing permissions and
12  * limitations under the License.
13  */

14 package net.logstash.logback.encoder;
15
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.OutputStream;
19 import java.nio.charset.Charset;
20
21 import com.fasterxml.jackson.databind.annotation.JsonValueInstantiator;
22
23 import net.logstash.logback.Logback11Support;
24 import net.logstash.logback.composite.CompositeJsonFormatter;
25 import net.logstash.logback.composite.JsonProviders;
26 import net.logstash.logback.decorate.JsonFactoryDecorator;
27 import net.logstash.logback.decorate.JsonGeneratorDecorator;
28 import ch.qos.logback.core.encoder.Encoder;
29 import ch.qos.logback.core.encoder.EncoderBase;
30 import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
31 import ch.qos.logback.core.pattern.PatternLayoutBase;
32 import ch.qos.logback.core.spi.DeferredProcessingAware;
33
34 public abstract class CompositeJsonEncoder<Event extends DeferredProcessingAware>
35         extends EncoderBase<Event> {
36     
37     private static final byte[] EMPTY_BYTES = new byte[0];
38     
39     /**
40      * Determines whether the {@link #logback11OutputStream} should be flushed after each event is encoded.
41      * Only applicable to logback versions less than or equal to 1.1.x.
42      */

43     private boolean logback11ImmediateFlush = true;
44     /**
45      * The underlying output stream to which to send encoded output.
46      * Only applicable to logback versions less than or equal to 1.1.x.
47      */

48     private OutputStream logback11OutputStream;
49     
50     /**
51      * The minimum size of the byte array buffer used when 
52      * encoding events in logback versions greater than or equal to 1.2.0.
53      * 
54      * The actual buffer size will be the {@link #minBufferSize}
55      * plus the prefix, suffix, and line separators sizes.
56      */

57     private int minBufferSize = 1024;
58     
59     private Encoder<Event> prefix;
60     private Encoder<Event> suffix;
61     
62     private final CompositeJsonFormatter<Event> formatter;
63     
64     private String lineSeparator = System.lineSeparator();
65     
66     private byte[] lineSeparatorBytes;
67     
68     private Charset charset;
69     
70     /**
71      * Logback 1.1 reflection support.
72      * This field is set by unit tests.
73      */

74     private Logback11Support logback11Support = Logback11Support.INSTANCE;
75
76     public CompositeJsonEncoder() {
77         super();
78         this.formatter = createFormatter();
79     }
80     
81     protected abstract CompositeJsonFormatter<Event> createFormatter();
82     
83     /**
84      * This is an overridden method from {@link Encoder} from logback 1.1.
85      * It sets the {@link OutputStream} to which this encoder should write encoded events.
86      * 
87      * This method is not part of the {@link Encoder} interface in logback 1.2,
88      * therefore, logback 1.2+ will not call this method.
89      * 
90      * @throws IllegalStateException if the logback version is &gt;= 1.2
91      */

92     public void init(OutputStream outputStream) throws IOException {
93         logback11Support.verifyLogback11OrBefore();
94         this.logback11OutputStream = outputStream;
95         initWrapped(prefix, outputStream);
96         initWrapped(suffix, outputStream);
97     }
98
99     private void initWrapped(Encoder<Event> wrapped, OutputStream outputStream) throws IOException {
100         if (wrapped != null) {
101             logback11Support.init(wrapped, outputStream);
102         }
103     }
104     
105     @Override
106     public byte[] encode(Event event) {
107         logback11Support.verifyLogback12OrAfter();
108         
109         byte[] prefixBytes = doEncodeWrappedToBytes(prefix, event);
110         byte[] suffixBytes = doEncodeWrappedToBytes(suffix, event);
111         
112         ByteArrayOutputStream outputStream = new ByteArrayOutputStream(
113                 minBufferSize
114                 + (prefixBytes == null ? 0 : prefixBytes.length)
115                 + (suffixBytes == null ? 0 : suffixBytes.length)
116                 + lineSeparatorBytes.length);
117         try {
118             if (prefixBytes != null) {
119                 outputStream.write(prefixBytes);
120             }   
121             
122             formatter.writeEventToOutputStream(event, outputStream);
123             
124             if (suffixBytes != null) {
125                 outputStream.write(suffixBytes);
126             }
127             
128             outputStream.write(lineSeparatorBytes);
129             
130             return outputStream.toByteArray();
131         } catch (IOException e) {
132             addWarn("Error encountered while encoding log event. "
133                     + "Event: " + event, e);
134             return EMPTY_BYTES;
135         } finally {
136             try {
137                 outputStream.close();
138             } catch (IOException e) {
139                 throw new RuntimeException(e);
140             }
141         }
142     }
143     
144     /**
145      * This is an overridden method from {@link Encoder} from logback 1.1.
146      * It encodes the event to the {@link OutputStream} passed to
147      * the {@link #init(OutputStream)} method.
148      * 
149      * This method takes the place of the bridge method that would have
150      * been generated by the compiler when compiled against logback 1.1.
151      * 
152      * This method is not part of the {@link Encoder} interface in logback 1.2,
153      * therefore, logback 1.2+ will not call this method.
154      *   
155      * @throws IllegalStateException if the logback version is &gt;= 1.2
156      */

157     @SuppressWarnings("unchecked")
158     public void doEncode(Object event) throws IOException {
159         doEncode((Event) event);
160     }
161
162     /**
163      * This is an overridden method from {@link Encoder} from logback 1.1.
164      * It encodes the event to the {@link OutputStream} passed to
165      * the {@link #init(OutputStream)} method.
166      * 
167      * This method is not part of the {@link Encoder} interface in logback 1.2,
168      * therefore, logback 1.2+ will not call this method.
169      *   
170      * @throws IllegalStateException if the logback version is &gt;= 1.2
171      */

172     public void doEncode(Event event) throws IOException {
173         logback11Support.verifyLogback11OrBefore();
174         try {
175             doEncodeWrappedToOutputStream(prefix, event);
176             
177             formatter.writeEventToOutputStream(event, logback11OutputStream);
178     
179             doEncodeWrappedToOutputStream(suffix, event);
180             
181             logback11OutputStream.write(lineSeparatorBytes);
182             
183             if (logback11ImmediateFlush) {
184                 logback11OutputStream.flush();
185             }
186         
187         } catch (IOException e) {
188             addWarn("Error encountered while encoding log event. "
189                     + "OutputStream is now in an unknown state, but will continue to be used for future log events."
190                     + "Event: " + event, e);
191         }
192     }
193
194     private byte[] doEncodeWrappedToBytes(Encoder<Event> wrapped, Event event) {
195         if (wrapped != null) {
196             return wrapped.encode(event);
197         }
198         return EMPTY_BYTES;
199     }
200     
201     private void doEncodeWrappedToOutputStream(Encoder<Event> wrapped, Event event) throws IOException {
202         if (wrapped != null) {
203             logback11Support.doEncode(wrapped, event);
204         }
205     }
206     
207     @Override
208     public void start() {
209         super.start();
210         formatter.setContext(getContext());
211         formatter.start();
212         charset = Charset.forName(formatter.getEncoding());
213         lineSeparatorBytes = this.lineSeparator == null
214                 ? EMPTY_BYTES
215                 : this.lineSeparator.getBytes(charset);
216         startWrapped(prefix);
217         startWrapped(suffix);
218         if (logback11Support.isLogback11OrBefore()) {
219             addWarn("Logback version is prior to 1.2.0.  Enabling backwards compatible encoding.  Logback 1.2.1 or greater is recommended.");
220         }
221     }
222
223     @SuppressWarnings({ "unchecked""rawtypes" })
224     private void startWrapped(Encoder<Event> wrapped) {
225         if (wrapped instanceof LayoutWrappingEncoder) {
226             /*
227              * Convenience hack to ensure the same charset is used in most cases.
228              * 
229              * The charset for other encoders must be configured
230              * on the wrapped encoder configuration.
231              */

232             LayoutWrappingEncoder<Event> layoutWrappedEncoder = (LayoutWrappingEncoder<Event>) wrapped;
233             layoutWrappedEncoder.setCharset(charset);
234             
235             if (layoutWrappedEncoder.getLayout() instanceof PatternLayoutBase) {
236                 /*
237                  * Don't ensure exception output (for ILoggingEvents)
238                  * or line separation (for IAccessEvents) 
239                  */

240                 PatternLayoutBase layout = (PatternLayoutBase) layoutWrappedEncoder.getLayout();
241                 layout.setPostCompileProcessor(null);
242                 /*
243                  * The pattern will be re-parsed during start.
244                  * Needed so that the pattern is re-parsed without
245                  * the postCompileProcessor.
246                  */

247                 layout.start();
248             }
249         }
250         
251         if (wrapped != null && !wrapped.isStarted()) {
252             wrapped.start();
253         }
254     }
255     
256     @Override
257     public void stop() {
258         super.stop();
259         formatter.stop();
260         stopWrapped(prefix);
261         stopWrapped(suffix);
262     }
263     
264     private void stopWrapped(Encoder<Event> wrapped) {
265         if (wrapped != null && !wrapped.isStarted()) {
266             wrapped.stop();
267         }
268     }
269     
270     /**
271      * This is an overridden method from {@link Encoder} from logback 1.1.
272      * It closes this encoder.
273      * It is called prior to closing the underlying outputstream.
274      * 
275      * This method is not part of the {@link Encoder} interface in logback 1.2,
276      * therefore, logback 1.2+ will not call this method.
277      *   
278      * @throws IllegalStateException if the logback version is &gt;= 1.2
279      */

280     public void close() throws IOException {
281         logback11Support.verifyLogback11OrBefore();
282         closeWrapped(prefix);
283         closeWrapped(suffix);
284     }
285     
286     private void closeWrapped(Encoder<Event> wrapped) throws IOException {
287         if (wrapped != null && !wrapped.isStarted()) {
288             logback11Support.close(wrapped);
289         }
290     }
291     
292     /*
293      * Visible for unit testing 
294      */

295     protected void setLogback11Support(Logback11Support logback11Support) {
296         this.logback11Support = logback11Support;
297     }
298     
299     @Override
300     public byte[] headerBytes() {
301         return EMPTY_BYTES;
302     }
303
304     @Override
305     public byte[] footerBytes() {
306         return EMPTY_BYTES;
307     }
308     
309     public JsonProviders<Event> getProviders() {
310         return formatter.getProviders();
311     }
312
313     public void setProviders(JsonProviders<Event> jsonProviders) {
314         formatter.setProviders(jsonProviders);
315     }
316     
317     public boolean isImmediateFlush() {
318         return logback11ImmediateFlush;
319     }
320     
321     public void setImmediateFlush(boolean immediateFlush) {
322         this.logback11ImmediateFlush = immediateFlush;
323     }
324     
325     public JsonFactoryDecorator getJsonFactoryDecorator() {
326         return formatter.getJsonFactoryDecorator();
327     }
328
329     public void setJsonFactoryDecorator(JsonFactoryDecorator jsonFactoryDecorator) {
330         formatter.setJsonFactoryDecorator(jsonFactoryDecorator);
331     }
332
333     public JsonGeneratorDecorator getJsonGeneratorDecorator() {
334         return formatter.getJsonGeneratorDecorator();
335     }
336     
337     public String getEncoding() {
338         return formatter.getEncoding();
339     }
340
341     /**
342      * The character encoding to use (default = "<tt>UTF-8</tt>").
343      * Must an encoding supported by {@link com.fasterxml.jackson.core.JsonEncoding}
344      */

345     public void setEncoding(String encodingName) {
346         formatter.setEncoding(encodingName);
347     }
348
349     public void setFindAndRegisterJacksonModules(boolean findAndRegisterJacksonModules) {
350         formatter.setFindAndRegisterJacksonModules(findAndRegisterJacksonModules);
351     }
352
353     public void setJsonGeneratorDecorator(JsonGeneratorDecorator jsonGeneratorDecorator) {
354         formatter.setJsonGeneratorDecorator(jsonGeneratorDecorator);
355     }
356     
357     public String getLineSeparator() {
358         return lineSeparator;
359     }
360     
361     /**
362      * Sets which lineSeparator to use between events.
363      * <p>
364      * 
365      * The following values have special meaning:
366      * <ul>
367      * <li><tt>null</tt> or empty string = no new line.</li>
368      * <li>"<tt>SYSTEM</tt>" = operating system new line (default).</li>
369      * <li>"<tt>UNIX</tt>" = unix line ending (\n).</li>
370      * <li>"<tt>WINDOWS</tt>" = windows line ending (\r\n).</li>
371      * </ul>
372      * <p>
373      * Any other value will be used as given as the lineSeparator.
374      */

375     public void setLineSeparator(String lineSeparator) {
376         this.lineSeparator = SeparatorParser.parseSeparator(lineSeparator);
377     }
378     
379     public int getMinBufferSize() {
380         return minBufferSize;
381     }
382     /**
383      * Sets the minimum size of the byte array buffer used when 
384      * encoding events in logback versions greater than or equal to 1.2.0.
385      * 
386      * The actual buffer size will be the {@link #minBufferSize}
387      * plus the prefix, suffix, and line separators sizes.
388      */

389     public void setMinBufferSize(int minBufferSize) {
390         this.minBufferSize = minBufferSize;
391     }
392
393     protected CompositeJsonFormatter<Event> getFormatter() {
394         return formatter;
395     }
396     
397     public Encoder<Event> getPrefix() {
398         return prefix;
399     }
400     public void setPrefix(Encoder<Event> prefix) {
401         this.prefix = prefix;
402     }
403     
404     public Encoder<Event> getSuffix() {
405         return suffix;
406     }
407     public void setSuffix(Encoder<Event> suffix) {
408         this.suffix = suffix;
409     }
410
411 }
412