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
55
56 @Plugin(name = "RFC5424Layout", category = "Core", elementType = "layout", printObject = true)
57 public final class RFC5424Layout extends AbstractStringLayout {
58
59
60
61
62 public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
63
64
65
66 public static final String DEFAULT_ID = "Audit";
67
68
69
70 public static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n");
71
72
73
74 public static final Pattern PARAM_VALUE_ESCAPE_PATTERN = Pattern.compile("[\\\"\\]\\\\]");
75
76 private static final String DEFAULT_MDCID = "mdc";
77 private static final int TWO_DIGITS = 10;
78 private static final int THREE_DIGITS = 100;
79 private static final int MILLIS_PER_MINUTE = 60000;
80 private static final int MINUTES_PER_HOUR = 60;
81
82 private static final String COMPONENT_KEY = "RFC5424-Converter";
83
84 private final Facility facility;
85 private final String defaultId;
86 private final Integer enterpriseNumber;
87 private final boolean includeMDC;
88 private final String mdcId;
89 private final String localHostName;
90 private final String appName;
91 private final String messageId;
92 private final String configName;
93 private final String mdcPrefix;
94 private final String eventPrefix;
95 private final List<String> mdcExcludes;
96 private final List<String> mdcIncludes;
97 private final List<String> mdcRequired;
98 private final ListChecker checker;
99 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
178
179
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
197
198
199
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
212
213
214
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
435
436 private interface ListChecker {
437 boolean check(String key);
438 }
439
440
441
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
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
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
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
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 }