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 */ 017 package org.apache.logging.log4j.core.layout; 018 019 import java.util.HashMap; 020 import org.apache.logging.log4j.LoggingException; 021 import org.apache.logging.log4j.core.LogEvent; 022 import org.apache.logging.log4j.core.config.Configuration; 023 import org.apache.logging.log4j.core.config.plugins.Plugin; 024 import org.apache.logging.log4j.core.config.plugins.PluginAttr; 025 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; 026 import org.apache.logging.log4j.core.config.plugins.PluginFactory; 027 import org.apache.logging.log4j.core.helpers.Charsets; 028 import org.apache.logging.log4j.core.helpers.NetUtils; 029 import org.apache.logging.log4j.core.net.Facility; 030 import org.apache.logging.log4j.core.net.Priority; 031 import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; 032 import org.apache.logging.log4j.core.pattern.PatternFormatter; 033 import org.apache.logging.log4j.core.pattern.PatternParser; 034 import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter; 035 import org.apache.logging.log4j.message.Message; 036 import org.apache.logging.log4j.message.StructuredDataId; 037 import org.apache.logging.log4j.message.StructuredDataMessage; 038 039 import java.nio.charset.Charset; 040 import java.util.ArrayList; 041 import java.util.Calendar; 042 import java.util.GregorianCalendar; 043 import java.util.List; 044 import java.util.Map; 045 import java.util.SortedMap; 046 import java.util.TreeMap; 047 import java.util.regex.Matcher; 048 import java.util.regex.Pattern; 049 050 051 /** 052 * Formats a log event in accordance with RFC 5424. 053 * 054 * @see <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a> 055 */ 056 @Plugin(name = "RFC5424Layout", category = "Core", elementType = "layout", printObject = true) 057 public final class RFC5424Layout extends AbstractStringLayout { 058 059 /** 060 * Not a very good default - it is the Apache Software Foundation's enterprise number. 061 */ 062 public static final int DEFAULT_ENTERPRISE_NUMBER = 18060; 063 /** 064 * The default event id. 065 */ 066 public static final String DEFAULT_ID = "Audit"; 067 /** 068 * Match newlines in a platform-independent manner. 069 */ 070 public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n"); 071 /** 072 * Match characters which require escaping 073 */ 074 public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]"); 075 076 private static final String DEFAULT_MDCID = "mdc"; 077 private static final int TWO_DIGITS = 10; 078 private static final int THREE_DIGITS = 100; 079 private static final int MILLIS_PER_MINUTE = 60000; 080 private static final int MINUTES_PER_HOUR = 60; 081 082 private static final String COMPONENT_KEY = "RFC5424-Converter"; 083 084 private final Facility facility; 085 private final String defaultId; 086 private final Integer enterpriseNumber; 087 private final boolean includeMDC; 088 private final String mdcId; 089 private final String localHostName; 090 private final String appName; 091 private final String messageId; 092 private final String configName; 093 private final String mdcPrefix; 094 private final String eventPrefix; 095 private final List<String> mdcExcludes; 096 private final List<String> mdcIncludes; 097 private final List<String> mdcRequired; 098 private final ListChecker checker; 099 private final ListChecker noopChecker = new NoopChecker(); 100 private final boolean includeNewLine; 101 private final String escapeNewLine; 102 103 private long lastTimestamp = -1; 104 private String timestamppStr; 105 106 private final List<PatternFormatter> formatters; 107 108 private RFC5424Layout(final Configuration config, final Facility facility, final String id, final int ein, 109 final boolean includeMDC, final boolean includeNL, final String escapeNL, final String mdcId, 110 final String mdcPrefix, final String eventPrefix, 111 final String appName, final String messageId, final String excludes, final String includes, 112 final String required, final Charset charset, final String exceptionPattern) { 113 super(charset); 114 final PatternParser parser = createPatternParser(config); 115 formatters = exceptionPattern == null ? null : parser.parse(exceptionPattern, false); 116 this.facility = facility; 117 this.defaultId = id == null ? DEFAULT_ID : id; 118 this.enterpriseNumber = ein; 119 this.includeMDC = includeMDC; 120 this.includeNewLine = includeNL; 121 this.escapeNewLine = escapeNL == null ? null : Matcher.quoteReplacement(escapeNL); 122 this.mdcId = mdcId; 123 this.mdcPrefix = mdcPrefix; 124 this.eventPrefix = eventPrefix; 125 this.appName = appName; 126 this.messageId = messageId; 127 this.localHostName = NetUtils.getLocalHostname(); 128 ListChecker c = null; 129 if (excludes != null) { 130 final String[] array = excludes.split(","); 131 if (array.length > 0) { 132 c = new ExcludeChecker(); 133 mdcExcludes = new ArrayList<String>(array.length); 134 for (final String str : array) { 135 mdcExcludes.add(str.trim()); 136 } 137 } else { 138 mdcExcludes = null; 139 } 140 } else { 141 mdcExcludes = null; 142 } 143 if (includes != null) { 144 final String[] array = includes.split(","); 145 if (array.length > 0) { 146 c = new IncludeChecker(); 147 mdcIncludes = new ArrayList<String>(array.length); 148 for (final String str : array) { 149 mdcIncludes.add(str.trim()); 150 } 151 } else { 152 mdcIncludes = null; 153 } 154 } else { 155 mdcIncludes = null; 156 } 157 if (required != null) { 158 final String[] array = required.split(","); 159 if (array.length > 0) { 160 mdcRequired = new ArrayList<String>(array.length); 161 for (final String str : array) { 162 mdcRequired.add(str.trim()); 163 } 164 } else { 165 mdcRequired = null; 166 } 167 168 } else { 169 mdcRequired = null; 170 } 171 this.checker = c != null ? c : noopChecker; 172 final String name = config == null ? null : config.getName(); 173 configName = name != null && name.length() > 0 ? name : null; 174 } 175 176 /** 177 * Create a PatternParser. 178 * @param config The Configuration. 179 * @return The PatternParser. 180 */ 181 public static PatternParser createPatternParser(final Configuration config) { 182 if (config == null) { 183 return new PatternParser(config, PatternLayout.KEY, LogEventPatternConverter.class, 184 ThrowablePatternConverter.class); 185 } 186 PatternParser parser = (PatternParser) config.getComponent(COMPONENT_KEY); 187 if (parser == null) { 188 parser = new PatternParser(config, PatternLayout.KEY, ThrowablePatternConverter.class); 189 config.addComponent(COMPONENT_KEY, parser); 190 parser = (PatternParser) config.getComponent(COMPONENT_KEY); 191 } 192 return parser; 193 } 194 195 /** 196 * RFC5424Layout's content format is specified by:<p/> 197 * Key: "structured" Value: "true"<p/> 198 * Key: "format" Value: "RFC5424"<p/> 199 * @return Map of content format keys supporting RFC5424Layout 200 */ 201 @Override 202 public Map<String, String> getContentFormat() 203 { 204 Map<String, String> result = new HashMap<String, String>(); 205 result.put("structured", "true"); 206 result.put("formatType", "RFC5424"); 207 return result; 208 } 209 210 /** 211 * Formats a {@link org.apache.logging.log4j.core.LogEvent} in conformance with the RFC 5424 Syslog specification. 212 * 213 * @param event The LogEvent. 214 * @return The RFC 5424 String representation of the LogEvent. 215 */ 216 @Override 217 public String toSerializable(final LogEvent event) { 218 final Message msg = event.getMessage(); 219 final boolean isStructured = msg instanceof StructuredDataMessage; 220 final StringBuilder buf = new StringBuilder(); 221 222 buf.append("<"); 223 buf.append(Priority.getPriority(facility, event.getLevel())); 224 buf.append(">1 "); 225 buf.append(computeTimeStampString(event.getMillis())); 226 buf.append(' '); 227 buf.append(localHostName); 228 buf.append(' '); 229 if (appName != null) { 230 buf.append(appName); 231 } else if (configName != null) { 232 buf.append(configName); 233 } else { 234 buf.append("-"); 235 } 236 buf.append(" "); 237 buf.append(getProcId()); 238 buf.append(" "); 239 final String type = isStructured ? ((StructuredDataMessage) msg).getType() : null; 240 if (type != null) { 241 buf.append(type); 242 } else if (messageId != null) { 243 buf.append(messageId); 244 } else { 245 buf.append("-"); 246 } 247 buf.append(" "); 248 if (isStructured || includeMDC) { 249 StructuredDataId id = null; 250 String text; 251 if (isStructured) { 252 final StructuredDataMessage data = (StructuredDataMessage) msg; 253 final Map<String, String> map = data.getData(); 254 id = data.getId(); 255 formatStructuredElement(id, eventPrefix, map, buf, noopChecker); 256 text = data.getFormat(); 257 } else { 258 text = msg.getFormattedMessage(); 259 } 260 if (includeMDC) { 261 Map<String, String> map = event.getContextMap(); 262 if (mdcRequired != null) { 263 checkRequired(map); 264 } 265 final int ein = id == null || id.getEnterpriseNumber() < 0 ? 266 enterpriseNumber : id.getEnterpriseNumber(); 267 final StructuredDataId mdcSDID = new StructuredDataId(mdcId, ein, null, null); 268 formatStructuredElement(mdcSDID, mdcPrefix, map, buf, checker); 269 } 270 if (text != null && text.length() > 0) { 271 buf.append(" ").append(escapeNewlines(text, escapeNewLine)); 272 } 273 } else { 274 buf.append("- "); 275 buf.append(escapeNewlines(msg.getFormattedMessage(), escapeNewLine)); 276 } 277 if (formatters != null && event.getThrown() != null) { 278 final StringBuilder exception = new StringBuilder("\n"); 279 for (final PatternFormatter formatter : formatters) { 280 formatter.format(event, exception); 281 } 282 buf.append(escapeNewlines(exception.toString(), escapeNewLine)); 283 } 284 if (includeNewLine) { 285 buf.append("\n"); 286 } 287 return buf.toString(); 288 } 289 290 private String escapeNewlines(final String text, final String escapeNewLine) 291 { 292 if (null == escapeNewLine) { 293 return text; 294 } 295 return NEWLINE_PATTERN.matcher(text).replaceAll(escapeNewLine); 296 } 297 298 protected String getProcId() { 299 return "-"; 300 } 301 302 protected List<String> getMdcExcludes() { 303 return mdcExcludes; 304 } 305 306 protected List<String> getMdcIncludes() { 307 return mdcIncludes; 308 } 309 310 private String computeTimeStampString(final long now) { 311 long last; 312 synchronized (this) { 313 last = lastTimestamp; 314 if (now == lastTimestamp) { 315 return timestamppStr; 316 } 317 } 318 319 final StringBuilder buf = new StringBuilder(); 320 final Calendar cal = new GregorianCalendar(); 321 cal.setTimeInMillis(now); 322 buf.append(Integer.toString(cal.get(Calendar.YEAR))); 323 buf.append("-"); 324 pad(cal.get(Calendar.MONTH) + 1, TWO_DIGITS, buf); 325 buf.append("-"); 326 pad(cal.get(Calendar.DAY_OF_MONTH), TWO_DIGITS, buf); 327 buf.append("T"); 328 pad(cal.get(Calendar.HOUR_OF_DAY), TWO_DIGITS, buf); 329 buf.append(":"); 330 pad(cal.get(Calendar.MINUTE), TWO_DIGITS, buf); 331 buf.append(":"); 332 pad(cal.get(Calendar.SECOND), TWO_DIGITS, buf); 333 334 final int millis = cal.get(Calendar.MILLISECOND); 335 if (millis != 0) { 336 buf.append('.'); 337 pad(millis, THREE_DIGITS, buf); 338 } 339 340 int tzmin = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / MILLIS_PER_MINUTE; 341 if (tzmin == 0) { 342 buf.append("Z"); 343 } else { 344 if (tzmin < 0) { 345 tzmin = -tzmin; 346 buf.append("-"); 347 } else { 348 buf.append("+"); 349 } 350 final int tzhour = tzmin / MINUTES_PER_HOUR; 351 tzmin -= tzhour * MINUTES_PER_HOUR; 352 pad(tzhour, TWO_DIGITS, buf); 353 buf.append(":"); 354 pad(tzmin, TWO_DIGITS, buf); 355 } 356 synchronized (this) { 357 if (last == lastTimestamp) { 358 lastTimestamp = now; 359 timestamppStr = buf.toString(); 360 } 361 } 362 return buf.toString(); 363 } 364 365 private void pad(final int val, int max, final StringBuilder buf) { 366 while (max > 1) { 367 if (val < max) { 368 buf.append("0"); 369 } 370 max = max / TWO_DIGITS; 371 } 372 buf.append(Integer.toString(val)); 373 } 374 375 private void formatStructuredElement(final StructuredDataId id, final String prefix, final Map<String, String> data, 376 final StringBuilder sb, final ListChecker checker) { 377 if (id == null && defaultId == null) { 378 return; 379 } 380 sb.append("["); 381 sb.append(getId(id)); 382 appendMap(prefix, data, sb, checker); 383 sb.append("]"); 384 } 385 386 private String getId(final StructuredDataId id) { 387 final StringBuilder sb = new StringBuilder(); 388 if (id.getName() == null) { 389 sb.append(defaultId); 390 } else { 391 sb.append(id.getName()); 392 } 393 int ein = id.getEnterpriseNumber(); 394 if (ein < 0) { 395 ein = enterpriseNumber; 396 } 397 if (ein >= 0) { 398 sb.append("@").append(ein); 399 } 400 return sb.toString(); 401 } 402 403 private void checkRequired(final Map<String, String> map) { 404 for (final String key : mdcRequired) { 405 final String value = map.get(key); 406 if (value == null) { 407 throw new LoggingException("Required key " + key + " is missing from the " + mdcId); 408 } 409 } 410 } 411 412 private void appendMap(final String prefix, final Map<String, String> map, final StringBuilder sb, 413 final ListChecker checker) 414 { 415 final SortedMap<String, String> sorted = new TreeMap<String, String>(map); 416 for (final Map.Entry<String, String> entry : sorted.entrySet()) { 417 if (checker.check(entry.getKey()) && entry.getValue() != null) { 418 sb.append(" "); 419 if (prefix != null) { 420 sb.append(prefix); 421 } 422 sb.append(escapeNewlines(escapeSDParams(entry.getKey()), escapeNewLine)).append("=\"") 423 .append(escapeNewlines(escapeSDParams(entry.getValue()), escapeNewLine)).append("\""); 424 } 425 } 426 } 427 428 private String escapeSDParams(String value) 429 { 430 return PARAM_VALUE_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$0"); 431 } 432 433 /** 434 * Interface used to check keys in a Map. 435 */ 436 private interface ListChecker { 437 boolean check(String key); 438 } 439 440 /** 441 * Includes only the listed keys. 442 */ 443 private class IncludeChecker implements ListChecker { 444 @Override 445 public boolean check(final String key) { 446 return mdcIncludes.contains(key); 447 } 448 } 449 450 /** 451 * Excludes the listed keys. 452 */ 453 private class ExcludeChecker implements ListChecker { 454 @Override 455 public boolean check(final String key) { 456 return !mdcExcludes.contains(key); 457 } 458 } 459 460 /** 461 * Does nothing. 462 */ 463 private class NoopChecker implements ListChecker { 464 @Override 465 public boolean check(final String key) { 466 return true; 467 } 468 } 469 470 @Override 471 public String toString() { 472 final StringBuilder sb = new StringBuilder(); 473 sb.append("facility=").append(facility.name()); 474 sb.append(" appName=").append(appName); 475 sb.append(" defaultId=").append(defaultId); 476 sb.append(" enterpriseNumber=").append(enterpriseNumber); 477 sb.append(" newLine=").append(includeNewLine); 478 sb.append(" includeMDC=").append(includeMDC); 479 sb.append(" messageId=").append(messageId); 480 return sb.toString(); 481 } 482 483 /** 484 * Create the RFC 5424 Layout. 485 * @param facility The Facility is used to try to classify the message. 486 * @param id The default structured data id to use when formatting according to RFC 5424. 487 * @param ein The IANA enterprise number. 488 * @param includeMDC Indicates whether data from the ThreadContextMap will be included in the RFC 5424 Syslog 489 * record. Defaults to "true:. 490 * @param mdcId The id to use for the MDC Structured Data Element. 491 * @param mdcPrefix The prefix to add to MDC key names. 492 * @param eventPrefix The prefix to add to event key names. 493 * @param includeNL If true, a newline will be appended to the end of the syslog record. The default is false. 494 * @param escapeNL String that should be used to replace newlines within the message text. 495 * @param appName The value to use as the APP-NAME in the RFC 5424 syslog record. 496 * @param msgId The default value to be used in the MSGID field of RFC 5424 syslog records. 497 * @param excludes A comma separated list of mdc keys that should be excluded from the LogEvent. 498 * @param includes A comma separated list of mdc keys that should be included in the FlumeEvent. 499 * @param required A comma separated list of mdc keys that must be present in the MDC. 500 * @param exceptionPattern The pattern for formatting exceptions. 501 * @param config The Configuration. Some Converters require access to the Interpolator. 502 * @return An RFC5424Layout. 503 */ 504 @PluginFactory 505 public static RFC5424Layout createLayout(@PluginAttr("facility") final String facility, 506 @PluginAttr("id") final String id, 507 @PluginAttr("enterpriseNumber") final String ein, 508 @PluginAttr("includeMDC") final String includeMDC, 509 @PluginAttr("mdcId") String mdcId, 510 @PluginAttr("mdcPrefix") String mdcPrefix, 511 @PluginAttr("eventPrefix") String eventPrefix, 512 @PluginAttr("newLine") final String includeNL, 513 @PluginAttr("newLineEscape") final String escapeNL, 514 @PluginAttr("appName") final String appName, 515 @PluginAttr("messageId") final String msgId, 516 @PluginAttr("mdcExcludes") final String excludes, 517 @PluginAttr("mdcIncludes") String includes, 518 @PluginAttr("mdcRequired") final String required, 519 @PluginAttr("exceptionPattern") final String exceptionPattern, 520 @PluginConfiguration final Configuration config) { 521 final Charset charset = Charsets.UTF_8; 522 if (includes != null && excludes != null) { 523 LOGGER.error("mdcIncludes and mdcExcludes are mutually exclusive. Includes wil be ignored"); 524 includes = null; 525 } 526 final Facility f = Facility.toFacility(facility, Facility.LOCAL0); 527 final int enterpriseNumber = ein == null ? DEFAULT_ENTERPRISE_NUMBER : Integer.parseInt(ein); 528 final boolean isMdc = includeMDC == null ? true : Boolean.valueOf(includeMDC); 529 final boolean includeNewLine = includeNL == null ? false : Boolean.valueOf(includeNL); 530 if (mdcId == null) { 531 mdcId = DEFAULT_MDCID; 532 } 533 534 return new RFC5424Layout(config, f, id, enterpriseNumber, isMdc, includeNewLine, escapeNL, mdcId, mdcPrefix, 535 eventPrefix, appName, msgId, excludes, includes, required, charset, exceptionPattern); 536 } 537 }