001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.layout;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.io.PrintWriter;
023import java.io.StringWriter;
024import java.nio.charset.StandardCharsets;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.zip.DeflaterOutputStream;
031import java.util.zip.GZIPOutputStream;
032
033import org.apache.logging.log4j.Level;
034import org.apache.logging.log4j.core.Layout;
035import org.apache.logging.log4j.core.LogEvent;
036import org.apache.logging.log4j.core.config.Configuration;
037import org.apache.logging.log4j.core.config.Node;
038import org.apache.logging.log4j.core.config.plugins.Plugin;
039import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
040import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
041import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
042import org.apache.logging.log4j.core.config.plugins.PluginElement;
043import org.apache.logging.log4j.core.layout.internal.ExcludeChecker;
044import org.apache.logging.log4j.core.layout.internal.IncludeChecker;
045import org.apache.logging.log4j.core.layout.internal.ListChecker;
046import org.apache.logging.log4j.core.lookup.StrSubstitutor;
047import org.apache.logging.log4j.core.net.Severity;
048import org.apache.logging.log4j.core.util.JsonUtils;
049import org.apache.logging.log4j.core.util.KeyValuePair;
050import org.apache.logging.log4j.core.util.NetUtils;
051import org.apache.logging.log4j.core.util.Patterns;
052import org.apache.logging.log4j.message.MapMessage;
053import org.apache.logging.log4j.message.Message;
054import org.apache.logging.log4j.status.StatusLogger;
055import org.apache.logging.log4j.util.StringBuilderFormattable;
056import org.apache.logging.log4j.util.Strings;
057import org.apache.logging.log4j.util.TriConsumer;
058
059/**
060 * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
061 * <p>
062 * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if
063 * log event data is larger than 1024 bytes (the {@code compressionThreshold}).
064 * This layout does not implement chunking.
065 * </p>
066 *
067 * @see <a href="http://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a>
068 */
069@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
070public final class GelfLayout extends AbstractStringLayout {
071
072    public enum CompressionType {
073
074        GZIP {
075            @Override
076            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
077                return new GZIPOutputStream(os);
078            }
079        },
080        ZLIB {
081            @Override
082            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
083                return new DeflaterOutputStream(os);
084            }
085        },
086        OFF {
087            @Override
088            public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
089                return null;
090            }
091        };
092
093        public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
094    }
095
096    private static final char C = ',';
097    private static final int COMPRESSION_THRESHOLD = 1024;
098    private static final char Q = '\"';
099    private static final String QC = "\",";
100    private static final String QU = "\"_";
101
102    private final KeyValuePair[] additionalFields;
103    private final int compressionThreshold;
104    private final CompressionType compressionType;
105    private final String host;
106    private final boolean includeStacktrace;
107    private final boolean includeThreadContext;
108    private final boolean includeMapMessage;
109    private final boolean includeNullDelimiter;
110    private final boolean includeNewLineDelimiter;
111    private final boolean omitEmptyFields;
112    private final PatternLayout layout;
113    private final FieldWriter mdcWriter;
114    private final FieldWriter mapWriter;
115
116    public static class Builder<B extends Builder<B>> extends AbstractStringLayout.Builder<B>
117        implements org.apache.logging.log4j.core.util.Builder<GelfLayout> {
118
119        @PluginBuilderAttribute
120        private String host;
121
122        @PluginElement("AdditionalField")
123        private KeyValuePair[] additionalFields;
124
125        @PluginBuilderAttribute
126        private CompressionType compressionType = CompressionType.GZIP;
127
128        @PluginBuilderAttribute
129        private int compressionThreshold = COMPRESSION_THRESHOLD;
130
131        @PluginBuilderAttribute
132        private boolean includeStacktrace = true;
133
134        @PluginBuilderAttribute
135        private boolean includeThreadContext = true;
136
137        @PluginBuilderAttribute
138        private boolean includeNullDelimiter = false;
139
140        @PluginBuilderAttribute
141        private boolean includeNewLineDelimiter = false;
142
143        @PluginBuilderAttribute
144        private String threadContextIncludes = null;
145
146        @PluginBuilderAttribute
147        private String threadContextExcludes = null;
148
149        @PluginBuilderAttribute
150        private String mapMessageIncludes = null;
151
152        @PluginBuilderAttribute
153        private String mapMessageExcludes = null;
154
155        @PluginBuilderAttribute
156        private boolean includeMapMessage = true;
157
158        @PluginBuilderAttribute
159        private boolean omitEmptyFields = false;
160
161        @PluginBuilderAttribute
162        private String messagePattern = null;
163
164        @PluginBuilderAttribute
165        private String threadContextPrefix = "";
166
167        @PluginBuilderAttribute
168        private String mapPrefix = "";
169
170        @PluginElement("PatternSelector")
171        private PatternSelector patternSelector = null;
172
173        public Builder() {
174            setCharset(StandardCharsets.UTF_8);
175        }
176
177        @Override
178        public GelfLayout build() {
179            ListChecker mdcChecker = createChecker(threadContextExcludes, threadContextIncludes);
180            ListChecker mapChecker = createChecker(mapMessageExcludes, mapMessageIncludes);
181            PatternLayout patternLayout = null;
182            if (messagePattern != null && patternSelector != null) {
183                LOGGER.error("A message pattern and PatternSelector cannot both be specified on GelfLayout, "
184                        + "ignoring message pattern");
185                messagePattern = null;
186            }
187            if (messagePattern != null) {
188                patternLayout = PatternLayout.newBuilder().withPattern(messagePattern)
189                        .withAlwaysWriteExceptions(includeStacktrace)
190                        .withConfiguration(getConfiguration())
191                        .build();
192            }
193            if (patternSelector != null) {
194                patternLayout = PatternLayout.newBuilder().withPatternSelector(patternSelector)
195                        .withAlwaysWriteExceptions(includeStacktrace)
196                        .withConfiguration(getConfiguration())
197                        .build();
198            }
199            return new GelfLayout(getConfiguration(), host, additionalFields, compressionType, compressionThreshold,
200                    includeStacktrace, includeThreadContext, includeMapMessage, includeNullDelimiter,
201                    includeNewLineDelimiter, omitEmptyFields, mdcChecker, mapChecker, patternLayout,
202                    threadContextPrefix, mapPrefix);
203        }
204
205        private ListChecker createChecker(String excludes, String includes) {
206            ListChecker checker = null;
207            if (excludes != null) {
208                final String[] array = excludes.split(Patterns.COMMA_SEPARATOR);
209                if (array.length > 0) {
210                    List<String> excludeList = new ArrayList<>(array.length);
211                    for (final String str : array) {
212                        excludeList.add(str.trim());
213                    }
214                    checker = new ExcludeChecker(excludeList);
215                }
216            }
217            if (includes != null) {
218                final String[] array = includes.split(Patterns.COMMA_SEPARATOR);
219                if (array.length > 0) {
220                    List<String> includeList = new ArrayList<>(array.length);
221                    for (final String str : array) {
222                        includeList.add(str.trim());
223                    }
224                    checker = new IncludeChecker(includeList);
225                }
226            }
227            if (checker == null) {
228                checker = ListChecker.NOOP_CHECKER;
229            }
230            return checker;
231        }
232
233        public String getHost() {
234            return host;
235        }
236
237        public CompressionType getCompressionType() {
238            return compressionType;
239        }
240
241        public int getCompressionThreshold() {
242            return compressionThreshold;
243        }
244
245        public boolean isIncludeStacktrace() {
246            return includeStacktrace;
247        }
248
249        public boolean isIncludeThreadContext() {
250            return includeThreadContext;
251        }
252
253        public boolean isIncludeNullDelimiter() { return includeNullDelimiter; }
254
255        public boolean isIncludeNewLineDelimiter() {
256            return includeNewLineDelimiter;
257        }
258
259        public KeyValuePair[] getAdditionalFields() {
260            return additionalFields;
261        }
262
263        /**
264         * The value of the <code>host</code> property (optional, defaults to local host name).
265         *
266         * @return this builder
267         */
268        public B setHost(final String host) {
269            this.host = host;
270            return asBuilder();
271        }
272
273        /**
274         * Compression to use (optional, defaults to GZIP).
275         *
276         * @return this builder
277         */
278        public B setCompressionType(final CompressionType compressionType) {
279            this.compressionType = compressionType;
280            return asBuilder();
281        }
282
283        /**
284         * Compress if data is larger than this number of bytes (optional, defaults to 1024).
285         *
286         * @return this builder
287         */
288        public B setCompressionThreshold(final int compressionThreshold) {
289            this.compressionThreshold = compressionThreshold;
290            return asBuilder();
291        }
292
293        /**
294         * Whether to include full stacktrace of logged Throwables (optional, default to true).
295         * If set to false, only the class name and message of the Throwable will be included.
296         *
297         * @return this builder
298         */
299        public B setIncludeStacktrace(final boolean includeStacktrace) {
300            this.includeStacktrace = includeStacktrace;
301            return asBuilder();
302        }
303
304        /**
305         * Whether to include thread context as additional fields (optional, default to true).
306         *
307         * @return this builder
308         */
309        public B setIncludeThreadContext(final boolean includeThreadContext) {
310            this.includeThreadContext = includeThreadContext;
311            return asBuilder();
312        }
313
314        /**
315         * Whether to include NULL byte as delimiter after each event (optional, default to false).
316         * Useful for Graylog GELF TCP input.
317         *
318         * @return this builder
319         */
320        public B setIncludeNullDelimiter(final boolean includeNullDelimiter) {
321            this.includeNullDelimiter = includeNullDelimiter;
322            return asBuilder();
323        }
324
325        /**
326         * Whether to include newline (LF) as delimiter after each event (optional, default to false).
327         *
328         * @return this builder
329         */
330        public B setIncludeNewLineDelimiter(final boolean includeNewLineDelimiter) {
331            this.includeNewLineDelimiter = includeNewLineDelimiter;
332            return asBuilder();
333        }
334
335        /**
336         * Additional fields to set on each log event.
337         *
338         * @return this builder
339         */
340        public B setAdditionalFields(final KeyValuePair[] additionalFields) {
341            this.additionalFields = additionalFields;
342            return asBuilder();
343        }
344
345        /**
346         * The pattern to use to format the message.
347         * @param pattern the pattern string.
348         * @return this builder
349         */
350        public B setMessagePattern(final String pattern) {
351            this.messagePattern = pattern;
352            return asBuilder();
353        }
354
355        /**
356         * The PatternSelector to use to format the message.
357         * @param patternSelector the PatternSelector.
358         * @return this builder
359         */
360        public B setPatternSelector(final PatternSelector patternSelector) {
361            this.patternSelector = patternSelector;
362            return asBuilder();
363        }
364
365        /**
366         * A comma separated list of thread context keys to include;
367         * @param mdcIncludes the list of keys.
368         * @return this builder
369         */
370        public B setMdcIncludes(final String mdcIncludes) {
371            this.threadContextIncludes = mdcIncludes;
372            return asBuilder();
373        }
374
375        /**
376         * A comma separated list of thread context keys to include;
377         * @param mdcExcludes the list of keys.
378         * @return this builder
379         */
380        public B setMdcExcludes(final String mdcExcludes) {
381            this.threadContextExcludes = mdcExcludes;
382            return asBuilder();
383        }
384
385        /**
386         * Whether to include MapMessage fields as additional fields (optional, default to true).
387         *
388         * @return this builder
389         */
390        public B setIncludeMapMessage(final boolean includeMapMessage) {
391            this.includeMapMessage = includeMapMessage;
392            return asBuilder();
393        }
394
395        /**
396         * A comma separated list of thread context keys to include;
397         * @param mapMessageIncludes the list of keys.
398         * @return this builder
399         */
400        public B setMapMessageIncludes(final String mapMessageIncludes) {
401            this.mapMessageIncludes = mapMessageIncludes;
402            return asBuilder();
403        }
404
405        /**
406         * A comma separated list of MapMessage keys to exclude;
407         * @param mapMessageExcludes the list of keys.
408         * @return this builder
409         */
410        public B setMapMessageExcludes(final String mapMessageExcludes) {
411            this.mapMessageExcludes = mapMessageExcludes;
412            return asBuilder();
413        }
414
415        /**
416         * The String to prefix the ThreadContext attributes.
417         * @param prefix The prefix value. Null values will be ignored.
418         * @return this builder.
419         */
420        public B setThreadContextPrefix(final String prefix) {
421            if (prefix != null) {
422                this.threadContextPrefix = prefix;
423            }
424            return asBuilder();
425        }
426
427        /**
428         * The String to prefix the MapMessage attributes.
429         * @param prefix The prefix value. Null values will be ignored.
430         * @return this builder.
431         */
432        public B setMapPrefix(final String prefix) {
433            if (prefix != null) {
434                this.mapPrefix = prefix;
435            }
436            return asBuilder();
437        }
438    }
439
440    /**
441     * @deprecated Use {@link #newBuilder()} instead
442     */
443    @Deprecated
444    public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
445                      final int compressionThreshold, final boolean includeStacktrace) {
446        this(null, host, additionalFields, compressionType, compressionThreshold, includeStacktrace, true, true,
447                false, false, false, null, null, null, "", "");
448    }
449
450    private GelfLayout(final Configuration config, final String host, final KeyValuePair[] additionalFields,
451            final CompressionType compressionType, final int compressionThreshold, final boolean includeStacktrace,
452            final boolean includeThreadContext, final boolean includeMapMessage, final boolean includeNullDelimiter,
453            final boolean includeNewLineDelimiter, final boolean omitEmptyFields, final ListChecker mdcChecker,
454            final ListChecker mapChecker, final PatternLayout patternLayout, final String mdcPrefix,
455            final String mapPrefix) {
456        super(config, StandardCharsets.UTF_8, null, null);
457        this.host = host != null ? host : NetUtils.getLocalHostname();
458        this.additionalFields = additionalFields != null ? additionalFields : KeyValuePair.EMPTY_ARRAY;
459        if (config == null) {
460            for (final KeyValuePair additionalField : this.additionalFields) {
461                if (valueNeedsLookup(additionalField.getValue())) {
462                    throw new IllegalArgumentException("configuration needs to be set when there are additional fields with variables");
463                }
464            }
465        }
466        this.compressionType = compressionType;
467        this.compressionThreshold = compressionThreshold;
468        this.includeStacktrace = includeStacktrace;
469        this.includeThreadContext = includeThreadContext;
470        this.includeMapMessage = includeMapMessage;
471        this.includeNullDelimiter = includeNullDelimiter;
472        this.includeNewLineDelimiter = includeNewLineDelimiter;
473        this.omitEmptyFields = omitEmptyFields;
474        if (includeNullDelimiter && compressionType != CompressionType.OFF) {
475            throw new IllegalArgumentException("null delimiter cannot be used with compression");
476        }
477        this.mdcWriter = new FieldWriter(mdcChecker, mdcPrefix);
478        this.mapWriter = new FieldWriter(mapChecker, mapPrefix);
479        this.layout = patternLayout;
480    }
481
482    @Override
483    public String toString() {
484        StringBuilder sb = new StringBuilder();
485        sb.append("host=").append(host);
486        sb.append(", compressionType=").append(compressionType.toString());
487        sb.append(", compressionThreshold=").append(compressionThreshold);
488        sb.append(", includeStackTrace=").append(includeStacktrace);
489        sb.append(", includeThreadContext=").append(includeThreadContext);
490        sb.append(", includeNullDelimiter=").append(includeNullDelimiter);
491        sb.append(", includeNewLineDelimiter=").append(includeNewLineDelimiter);
492        String threadVars = mdcWriter.getChecker().toString();
493        if (threadVars.length() > 0) {
494            sb.append(", ").append(threadVars);
495        }
496        String mapVars = mapWriter.getChecker().toString();
497        if (mapVars.length() > 0) {
498            sb.append(", ").append(mapVars);
499        }
500        if (layout != null) {
501            sb.append(", PatternLayout{").append(layout.toString()).append("}");
502        }
503        return sb.toString();
504    }
505
506    /**
507     * @deprecated Use {@link #newBuilder()} instead
508     */
509    @Deprecated
510    public static GelfLayout createLayout(
511            //@formatter:off
512            @PluginAttribute("host") final String host,
513            @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
514            @PluginAttribute(value = "compressionType",
515                defaultString = "GZIP") final CompressionType compressionType,
516            @PluginAttribute(value = "compressionThreshold",
517                defaultInt = COMPRESSION_THRESHOLD) final int compressionThreshold,
518            @PluginAttribute(value = "includeStacktrace",
519                defaultBoolean = true) final boolean includeStacktrace) {
520            // @formatter:on
521        return new GelfLayout(null, host, additionalFields, compressionType, compressionThreshold, includeStacktrace,
522                true, true, false, false, false, null, null, null, "", "");
523    }
524
525    @PluginBuilderFactory
526    public static <B extends Builder<B>> B newBuilder() {
527        return new Builder<B>().asBuilder();
528    }
529
530    @Override
531    public Map<String, String> getContentFormat() {
532        return Collections.emptyMap();
533    }
534
535    @Override
536    public String getContentType() {
537        return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
538    }
539
540    @Override
541    public byte[] toByteArray(final LogEvent event) {
542        final StringBuilder text = toText(event, getStringBuilder(), false);
543        final byte[] bytes = getBytes(text.toString());
544        return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
545    }
546
547    @Override
548    public void encode(final LogEvent event, final ByteBufferDestination destination) {
549        if (compressionType != CompressionType.OFF) {
550            super.encode(event, destination);
551            return;
552        }
553        final StringBuilder text = toText(event, getStringBuilder(), true);
554        final Encoder<StringBuilder> helper = getStringBuilderEncoder();
555        helper.encode(text, destination);
556    }
557
558    @Override
559    public boolean requiresLocation() {
560        return Objects.nonNull(layout) && layout.requiresLocation();
561    }
562
563    private byte[] compress(final byte[] bytes) {
564        try {
565            final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
566            try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
567                if (stream == null) {
568                    return bytes;
569                }
570                stream.write(bytes);
571                stream.finish();
572            }
573            return baos.toByteArray();
574        } catch (final IOException e) {
575            StatusLogger.getLogger().error(e);
576            return bytes;
577        }
578    }
579
580    @Override
581    public String toSerializable(final LogEvent event) {
582        final StringBuilder text = toText(event, getStringBuilder(), false);
583        return text.toString();
584    }
585
586    private StringBuilder toText(final LogEvent event, final StringBuilder builder, final boolean gcFree) {
587        builder.append('{');
588        builder.append("\"version\":\"1.1\",");
589        builder.append("\"host\":\"");
590        JsonUtils.quoteAsString(toNullSafeString(host), builder);
591        builder.append(QC);
592        builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
593        builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
594        if (event.getThreadName() != null) {
595            builder.append("\"_thread\":\"");
596            JsonUtils.quoteAsString(event.getThreadName(), builder);
597            builder.append(QC);
598        }
599        if (event.getLoggerName() != null) {
600            builder.append("\"_logger\":\"");
601            JsonUtils.quoteAsString(event.getLoggerName(), builder);
602            builder.append(QC);
603        }
604        if (additionalFields.length > 0) {
605            final StrSubstitutor strSubstitutor = getConfiguration().getStrSubstitutor();
606            for (final KeyValuePair additionalField : additionalFields) {
607                final String value = valueNeedsLookup(additionalField.getValue())
608                        ? strSubstitutor.replace(event, additionalField.getValue())
609                        : additionalField.getValue();
610                if (Strings.isNotEmpty(value) || !omitEmptyFields) {
611                    builder.append(QU);
612                    JsonUtils.quoteAsString(additionalField.getKey(), builder);
613                    builder.append("\":\"");
614                    JsonUtils.quoteAsString(toNullSafeString(value), builder);
615                    builder.append(QC);
616                }
617            }
618        }
619        if (includeThreadContext) {
620            event.getContextData().forEach(mdcWriter, builder);
621        }
622        if (includeMapMessage && event.getMessage() instanceof MapMessage) {
623            ((MapMessage<?, Object>) event.getMessage()).forEach((key, value) -> mapWriter.accept(key, value, builder));
624        }
625
626        if (event.getThrown() != null || layout != null) {
627            builder.append("\"full_message\":\"");
628            if (layout != null) {
629                final StringBuilder messageBuffer = getMessageStringBuilder();
630                layout.serialize(event, messageBuffer);
631                JsonUtils.quoteAsString(messageBuffer, builder);
632            } else if (includeStacktrace) {
633                JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
634            } else {
635                JsonUtils.quoteAsString(event.getThrown().toString(), builder);
636            }
637            builder.append(QC);
638        }
639
640        builder.append("\"short_message\":\"");
641        final Message message = event.getMessage();
642        if (message instanceof CharSequence) {
643            JsonUtils.quoteAsString(((CharSequence) message), builder);
644        } else if (gcFree && message instanceof StringBuilderFormattable) {
645            final StringBuilder messageBuffer = getMessageStringBuilder();
646            try {
647                ((StringBuilderFormattable) message).formatTo(messageBuffer);
648                JsonUtils.quoteAsString(messageBuffer, builder);
649            } finally {
650                trimToMaxSize(messageBuffer);
651            }
652        } else {
653            JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
654        }
655        builder.append(Q);
656        builder.append('}');
657        if (includeNullDelimiter) {
658            builder.append('\0');
659        }
660        if (includeNewLineDelimiter) {
661            builder.append('\n');
662        }
663        return builder;
664    }
665
666    private static boolean valueNeedsLookup(final String value) {
667        return value != null && value.contains("${");
668    }
669
670    private class FieldWriter implements TriConsumer<String, Object, StringBuilder> {
671        private final ListChecker checker;
672        private final String prefix;
673
674        FieldWriter(ListChecker checker, String prefix) {
675            this.checker = checker;
676            this.prefix = prefix;
677        }
678
679        @Override
680        public void accept(final String key, final Object value, final StringBuilder stringBuilder) {
681            String stringValue = String.valueOf(value);
682            if (checker.check(key) && (Strings.isNotEmpty(stringValue) || !omitEmptyFields)) {
683                stringBuilder.append(QU);
684                JsonUtils.quoteAsString(Strings.concat(prefix, key), stringBuilder);
685                stringBuilder.append("\":\"");
686                JsonUtils.quoteAsString(toNullSafeString(stringValue), stringBuilder);
687                stringBuilder.append(QC);
688            }
689        }
690
691        public ListChecker getChecker() {
692            return checker;
693        }
694    }
695
696    private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
697
698    private static StringBuilder getMessageStringBuilder() {
699        StringBuilder result = messageStringBuilder.get();
700        if (result == null) {
701            result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
702            messageStringBuilder.set(result);
703        }
704        result.setLength(0);
705        return result;
706    }
707
708    private static CharSequence toNullSafeString(final CharSequence s) {
709        return s == null ? Strings.EMPTY : s;
710    }
711
712    /**
713     * Non-private to make it accessible from unit test.
714     */
715    static CharSequence formatTimestamp(final long timeMillis) {
716        if (timeMillis < 1000) {
717            return "0";
718        }
719        final StringBuilder builder = getTimestampStringBuilder();
720        builder.append(timeMillis);
721        builder.insert(builder.length() - 3, '.');
722        return builder;
723    }
724
725    private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
726
727    private static StringBuilder getTimestampStringBuilder() {
728        StringBuilder result = timestampStringBuilder.get();
729        if (result == null) {
730            result = new StringBuilder(20);
731            timestampStringBuilder.set(result);
732        }
733        result.setLength(0);
734        return result;
735    }
736
737    /**
738     * http://en.wikipedia.org/wiki/Syslog#Severity_levels
739     */
740    private int formatLevel(final Level level) {
741        return Severity.getSeverity(level).getCode();
742    }
743
744    /**
745     * Non-private to make it accessible from unit test.
746     */
747    static CharSequence formatThrowable(final Throwable throwable) {
748        // stack traces are big enough to provide a reasonably large initial capacity here
749        final StringWriter sw = new StringWriter(2048);
750        final PrintWriter pw = new PrintWriter(sw);
751        throwable.printStackTrace(pw);
752        pw.flush();
753        return sw.getBuffer();
754    }
755}