1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.layout;
18
19 import java.util.HashMap;
20 import org.apache.logging.log4j.LoggingException;
21 import org.apache.logging.log4j.core.LogEvent;
22 import org.apache.logging.log4j.core.config.Configuration;
23 import org.apache.logging.log4j.core.config.plugins.Plugin;
24 import org.apache.logging.log4j.core.config.plugins.PluginAttr;
25 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
26 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
27 import org.apache.logging.log4j.core.helpers.Charsets;
28 import org.apache.logging.log4j.core.helpers.NetUtils;
29 import org.apache.logging.log4j.core.net.Facility;
30 import org.apache.logging.log4j.core.net.Priority;
31 import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
32 import org.apache.logging.log4j.core.pattern.PatternFormatter;
33 import org.apache.logging.log4j.core.pattern.PatternParser;
34 import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
35 import org.apache.logging.log4j.message.Message;
36 import org.apache.logging.log4j.message.StructuredDataId;
37 import org.apache.logging.log4j.message.StructuredDataMessage;
38
39 import java.nio.charset.Charset;
40 import java.util.ArrayList;
41 import java.util.Calendar;
42 import java.util.GregorianCalendar;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.SortedMap;
46 import java.util.TreeMap;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49
50
51
52
53
54 @Plugin(name = "RFC5424Layout", type = "Core", elementType = "layout", printObject = true)
55 public final class RFC5424Layout extends AbstractStringLayout {
56
57
58
59
60 public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
61
62
63
64 public static final String DEFAULT_ID = "Audit";
65
66
67
68 public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
69
70
71
72 public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
73
74 private static final String DEFAULT_MDCID = "mdc";
75 private static final int TWO_DIGITS = 10;
76 private static final int THREE_DIGITS = 100;
77 private static final int MILLIS_PER_MINUTE = 60000;
78 private static final int MINUTES_PER_HOUR = 60;
79
80 private static final String COMPONENT_KEY = "RFC5424-Converter";
81
82 private final Facility facility;
83 private final String defaultId;
84 private final Integer enterpriseNumber;
85 private final boolean includeMDC;
86 private final String mdcId;
87 private final String localHostName;
88 private final String appName;
89 private final String messageId;
90 private final String configName;
91 private final String mdcPrefix;
92 private final String eventPrefix;
93 private final List<String> mdcExcludes;
94 private final List<String> mdcIncludes;
95 private final List<String> mdcRequired;
96 private final ListChecker checker;
97 private final ListChecker noopChecker = new NoopChecker();
98 private final boolean includeNewLine;
99 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
176
177
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
195
196
197
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
209
210
211
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
431
432 private interface ListChecker {
433 boolean check(String key);
434 }
435
436
437
438
439 private class IncludeChecker implements ListChecker {
440 public boolean check(final String key) {
441 return mdcIncludes.contains(key);
442 }
443 }
444
445
446
447
448 private class ExcludeChecker implements ListChecker {
449 public boolean check(final String key) {
450 return !mdcExcludes.contains(key);
451 }
452 }
453
454
455
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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
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 }