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