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