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