/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.logstash.logback.encoder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; import net.logstash.logback.Logback11Support; import net.logstash.logback.composite.CompositeJsonFormatter; import net.logstash.logback.composite.JsonProviders; import net.logstash.logback.decorate.JsonFactoryDecorator; import net.logstash.logback.decorate.JsonGeneratorDecorator; import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; import ch.qos.logback.core.encoder.LayoutWrappingEncoder; import ch.qos.logback.core.pattern.PatternLayoutBase; import ch.qos.logback.core.spi.DeferredProcessingAware; public abstract class CompositeJsonEncoder<Event extends DeferredProcessingAware> extends EncoderBase<Event> { private static final byte[] EMPTY_BYTES = new byte[0]; /** * Determines whether the {@link #logback11OutputStream} should be flushed after each event is encoded. * Only applicable to logback versions less than or equal to 1.1.x. */ private boolean logback11ImmediateFlush = true; /** * The underlying output stream to which to send encoded output. * Only applicable to logback versions less than or equal to 1.1.x. */ private OutputStream logback11OutputStream; /** * The minimum size of the byte array buffer used when * encoding events in logback versions greater than or equal to 1.2.0. * * The actual buffer size will be the {@link #minBufferSize} * plus the prefix, suffix, and line separators sizes. */ private int minBufferSize = 1024; private Encoder<Event> prefix; private Encoder<Event> suffix; private final CompositeJsonFormatter<Event> formatter; private String lineSeparator = System.getProperty("line.separator"); private byte[] lineSeparatorBytes; private Charset charset; public CompositeJsonEncoder() { super(); this.formatter = createFormatter(); } protected abstract CompositeJsonFormatter<Event> createFormatter(); /** * This is an overridden method from {@link Encoder} from logback 1.1. * It sets the {@link OutputStream} to which this encoder should write encoded events. * * This method is not part of the {@link Encoder} interface in logback 1.2, * therefore, logback 1.2+ will not call this method. * * @throws IllegalStateException if the logback version is >= 1.2 */ public void init(OutputStream outputStream) throws IOException { Logback11Support.verifyLogback11OrBefore(); this.logback11OutputStream = outputStream; initWrapped(prefix, outputStream); initWrapped(suffix, outputStream); } private void initWrapped(Encoder<Event> wrapped, OutputStream outputStream) throws IOException { if (wrapped != null) { Logback11Support.init(wrapped, outputStream); } } @Override public byte[] encode(Event event) { Logback11Support.verifyLogback12OrAfter(); byte[] prefixBytes = doEncodeWrappedToBytes(prefix, event); byte[] suffixBytes = doEncodeWrappedToBytes(suffix, event); ByteArrayOutputStream outputStream = new ByteArrayOutputStream( minBufferSize + (prefixBytes == null ? 0 : prefixBytes.length) + (suffixBytes == null ? 0 : suffixBytes.length) + lineSeparatorBytes.length); try { if (prefixBytes != null) { outputStream.write(prefixBytes); } formatter.writeEventToOutputStream(event, outputStream); if (suffixBytes != null) { outputStream.write(suffixBytes); } outputStream.write(lineSeparatorBytes); return outputStream.toByteArray(); } catch (IOException e) { addWarn("Error encountered while encoding log event. " + "Event: " + event, e); return EMPTY_BYTES; } finally { try { outputStream.close(); } catch (IOException e) { throw new RuntimeException(e); } } } /** * This is an overridden method from {@link Encoder} from logback 1.1. * It encodes the event to the {@link OutputStream} passed to * the {@link #init(OutputStream)} method. * * This method takes the place of the bridge method that would have * been generated by the compiler when compiled against logback 1.1. * * This method is not part of the {@link Encoder} interface in logback 1.2, * therefore, logback 1.2+ will not call this method. * * @throws IllegalStateException if the logback version is >= 1.2 */ @SuppressWarnings("unchecked") public void doEncode(Object event) throws IOException { doEncode((Event) event); } /** * This is an overridden method from {@link Encoder} from logback 1.1. * It encodes the event to the {@link OutputStream} passed to * the {@link #init(OutputStream)} method. * * This method is not part of the {@link Encoder} interface in logback 1.2, * therefore, logback 1.2+ will not call this method. * * @throws IllegalStateException if the logback version is >= 1.2 */ public void doEncode(Event event) throws IOException { Logback11Support.verifyLogback11OrBefore(); try { doEncodeWrappedToOutputStream(prefix, event); formatter.writeEventToOutputStream(event, logback11OutputStream); doEncodeWrappedToOutputStream(suffix, event); logback11OutputStream.write(lineSeparatorBytes); if (logback11ImmediateFlush) { logback11OutputStream.flush(); } } catch (IOException e) { addWarn("Error encountered while encoding log event. " + "OutputStream is now in an unknown state, but will continue to be used for future log events." + "Event: " + event, e); } } private byte[] doEncodeWrappedToBytes(Encoder<Event> wrapped, Event event) { if (wrapped != null) { return wrapped.encode(event); } return EMPTY_BYTES; } private void doEncodeWrappedToOutputStream(Encoder<Event> wrapped, Event event) throws IOException { if (wrapped != null) { Logback11Support.doEncode(wrapped, event); } } @Override public void start() { super.start(); formatter.setContext(getContext()); formatter.start(); charset = Charset.forName(formatter.getEncoding()); lineSeparatorBytes = this.lineSeparator == null ? EMPTY_BYTES : this.lineSeparator.getBytes(charset); startWrapped(prefix); startWrapped(suffix); if (Logback11Support.isLogback11OrBefore()) { addWarn("Logback version is prior to 1.2.0. Enabling backwards compatible encoding. Logback 1.2.1 or greater is recommended."); } } @SuppressWarnings({ "unchecked", "rawtypes" }) private void startWrapped(Encoder<Event> wrapped) { if (wrapped instanceof LayoutWrappingEncoder) { /* * Convenience hack to ensure the same charset is used in most cases. * * The charset for other encoders must be configured * on the wrapped encoder configuration. */ LayoutWrappingEncoder<Event> layoutWrappedEncoder = (LayoutWrappingEncoder<Event>) wrapped; layoutWrappedEncoder.setCharset(charset); if (layoutWrappedEncoder.getLayout() instanceof PatternLayoutBase) { /* * Don't ensure exception output (for ILoggingEvents) * or line separation (for IAccessEvents) */ PatternLayoutBase layout = (PatternLayoutBase) layoutWrappedEncoder.getLayout(); layout.setPostCompileProcessor(null); /* * The pattern will be re-parsed during start. * Needed so that the pattern is re-parsed without * the postCompileProcessor. */ layout.start(); } } if (wrapped != null && !wrapped.isStarted()) { wrapped.start(); } } @Override public void stop() { super.stop(); formatter.stop(); stopWrapped(prefix); stopWrapped(suffix); } private void stopWrapped(Encoder<Event> wrapped) { if (wrapped != null && !wrapped.isStarted()) { wrapped.stop(); } } /** * This is an overridden method from {@link Encoder} from logback 1.1. * It closes this encoder. * It is called prior to closing the underlying outputstream. * * This method is not part of the {@link Encoder} interface in logback 1.2, * therefore, logback 1.2+ will not call this method. * * @throws IllegalStateException if the logback version is >= 1.2 */ public void close() throws IOException { Logback11Support.verifyLogback11OrBefore(); closeWrapped(prefix); closeWrapped(suffix); } private void closeWrapped(Encoder<Event> wrapped) throws IOException { if (wrapped != null && !wrapped.isStarted()) { Logback11Support.close(wrapped); } } @Override public byte[] headerBytes() { return EMPTY_BYTES; } @Override public byte[] footerBytes() { return EMPTY_BYTES; } public JsonProviders<Event> getProviders() { return formatter.getProviders(); } public void setProviders(JsonProviders<Event> jsonProviders) { formatter.setProviders(jsonProviders); } public boolean isImmediateFlush() { return logback11ImmediateFlush; } public void setImmediateFlush(boolean immediateFlush) { this.logback11ImmediateFlush = immediateFlush; } public JsonFactoryDecorator getJsonFactoryDecorator() { return formatter.getJsonFactoryDecorator(); } public void setJsonFactoryDecorator(JsonFactoryDecorator jsonFactoryDecorator) { formatter.setJsonFactoryDecorator(jsonFactoryDecorator); } public JsonGeneratorDecorator getJsonGeneratorDecorator() { return formatter.getJsonGeneratorDecorator(); } public String getEncoding() { return formatter.getEncoding(); } /** * The character encoding to use (default = "<tt>UTF-8</tt>"). * Must an encoding supported by {@link com.fasterxml.jackson.core.JsonEncoding} */ public void setEncoding(String encodingName) { formatter.setEncoding(encodingName); } public void setJsonGeneratorDecorator(JsonGeneratorDecorator jsonGeneratorDecorator) { formatter.setJsonGeneratorDecorator(jsonGeneratorDecorator); } public String getLineSeparator() { return lineSeparator; } /** * Sets which lineSeparator to use between events. * <p> * * The following values have special meaning: * <ul> * <li><tt>null</tt> or empty string = no new line.</li> * <li>"<tt>SYSTEM</tt>" = operating system new line (default).</li> * <li>"<tt>UNIX</tt>" = unix line ending (\n).</li> * <li>"<tt>WINDOWS</tt>" = windows line ending (\r\n).</li> * </ul> * <p> * Any other value will be used as given as the lineSeparator. */ public void setLineSeparator(String lineSeparator) { this.lineSeparator = SeparatorParser.parseSeparator(lineSeparator); } public int getMinBufferSize() { return minBufferSize; } /** * Sets the minimum size of the byte array buffer used when * encoding events in logback versions greater than or equal to 1.2.0. * * The actual buffer size will be the {@link #minBufferSize} * plus the prefix, suffix, and line separators sizes. */ public void setMinBufferSize(int minBufferSize) { this.minBufferSize = minBufferSize; } protected CompositeJsonFormatter<Event> getFormatter() { return formatter; } public Encoder<Event> getPrefix() { return prefix; } public void setPrefix(Encoder<Event> prefix) { this.prefix = prefix; } public Encoder<Event> getSuffix() { return suffix; } public void setSuffix(Encoder<Event> suffix) { this.suffix = suffix; } }