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}