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.io.IOException;
20 import java.io.InterruptedIOException;
21 import java.io.LineNumberReader;
22 import java.io.PrintWriter;
23 import java.io.StringReader;
24 import java.io.StringWriter;
25 import java.lang.management.ManagementFactory;
26 import java.nio.charset.Charset;
27 import java.nio.charset.StandardCharsets;
28 import java.util.ArrayList;
29
30 import org.apache.logging.log4j.Level;
31 import org.apache.logging.log4j.core.Layout;
32 import org.apache.logging.log4j.core.LogEvent;
33 import org.apache.logging.log4j.core.config.LoggerConfig;
34 import org.apache.logging.log4j.core.config.Node;
35 import org.apache.logging.log4j.core.config.plugins.Plugin;
36 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
37 import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
38 import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
39 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
40 import org.apache.logging.log4j.core.util.Transform;
41 import org.apache.logging.log4j.util.Strings;
42
43
44
45
46
47
48
49
50 @Plugin(name = "HtmlLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
51 public final class HtmlLayout extends AbstractStringLayout {
52
53
54
55
56 public static final String DEFAULT_FONT_FAMILY = "arial,sans-serif";
57
58 private static final String TRACE_PREFIX = "<br /> ";
59 private static final String REGEXP = Strings.LINE_SEPARATOR.equals("\n") ? "\n" : Strings.LINE_SEPARATOR + "|\n";
60 private static final String DEFAULT_TITLE = "Log4j Log Messages";
61 private static final String DEFAULT_CONTENT_TYPE = "text/html";
62
63 private final long jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
64
65
66 private final boolean locationInfo;
67 private final String title;
68 private final String contentType;
69 private final String font;
70 private final String fontSize;
71 private final String headerSize;
72
73
74 public static enum FontSize {
75 SMALLER("smaller"), XXSMALL("xx-small"), XSMALL("x-small"), SMALL("small"), MEDIUM("medium"), LARGE("large"),
76 XLARGE("x-large"), XXLARGE("xx-large"), LARGER("larger");
77
78 private final String size;
79
80 private FontSize(final String size) {
81 this.size = size;
82 }
83
84 public String getFontSize() {
85 return size;
86 }
87
88 public static FontSize getFontSize(final String size) {
89 for (final FontSize fontSize : values()) {
90 if (fontSize.size.equals(size)) {
91 return fontSize;
92 }
93 }
94 return SMALL;
95 }
96
97 public FontSize larger() {
98 return this.ordinal() < XXLARGE.ordinal() ? FontSize.values()[this.ordinal() + 1] : this;
99 }
100 }
101
102 private HtmlLayout(final boolean locationInfo, final String title, final String contentType, final Charset charset,
103 final String font, final String fontSize, final String headerSize) {
104 super(charset);
105 this.locationInfo = locationInfo;
106 this.title = title;
107 this.contentType = addCharsetToContentType(contentType);
108 this.font = font;
109 this.fontSize = fontSize;
110 this.headerSize = headerSize;
111 }
112
113
114
115
116 public String getTitle() {
117 return title;
118 }
119
120
121
122
123 public boolean isLocationInfo() {
124 return locationInfo;
125 }
126
127 @Override
128 public boolean requiresLocation() {
129 return locationInfo;
130 }
131
132 private String addCharsetToContentType(final String contentType) {
133 if (contentType == null) {
134 return DEFAULT_CONTENT_TYPE + "; charset=" + getCharset();
135 }
136 return contentType.contains("charset") ? contentType : contentType + "; charset=" + getCharset();
137 }
138
139
140
141
142
143
144
145 @Override
146 public String toSerializable(final LogEvent event) {
147 final StringBuilder sbuf = getStringBuilder();
148
149 sbuf.append(Strings.LINE_SEPARATOR).append("<tr>").append(Strings.LINE_SEPARATOR);
150
151 sbuf.append("<td>");
152 sbuf.append(event.getTimeMillis() - jvmStartTime);
153 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
154
155 final String escapedThread = Transform.escapeHtmlTags(event.getThreadName());
156 sbuf.append("<td title=\"").append(escapedThread).append(" thread\">");
157 sbuf.append(escapedThread);
158 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
159
160 sbuf.append("<td title=\"Level\">");
161 if (event.getLevel().equals(Level.DEBUG)) {
162 sbuf.append("<font color=\"#339933\">");
163 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
164 sbuf.append("</font>");
165 } else if (event.getLevel().isMoreSpecificThan(Level.WARN)) {
166 sbuf.append("<font color=\"#993300\"><strong>");
167 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
168 sbuf.append("</strong></font>");
169 } else {
170 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
171 }
172 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
173
174 String escapedLogger = Transform.escapeHtmlTags(event.getLoggerName());
175 if (Strings.isEmpty(escapedLogger)) {
176 escapedLogger = LoggerConfig.ROOT;
177 }
178 sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">");
179 sbuf.append(escapedLogger);
180 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
181
182 if (locationInfo) {
183 final StackTraceElement element = event.getSource();
184 sbuf.append("<td>");
185 sbuf.append(Transform.escapeHtmlTags(element.getFileName()));
186 sbuf.append(':');
187 sbuf.append(element.getLineNumber());
188 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
189 }
190
191 sbuf.append("<td title=\"Message\">");
192 sbuf.append(Transform.escapeHtmlTags(event.getMessage().getFormattedMessage()).replaceAll(REGEXP, "<br />"));
193 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
194 sbuf.append("</tr>").append(Strings.LINE_SEPARATOR);
195
196 if (event.getContextStack() != null && !event.getContextStack().isEmpty()) {
197 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize);
198 sbuf.append(";\" colspan=\"6\" ");
199 sbuf.append("title=\"Nested Diagnostic Context\">");
200 sbuf.append("NDC: ").append(Transform.escapeHtmlTags(event.getContextStack().toString()));
201 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR);
202 }
203
204 if (event.getContextData() != null && !event.getContextData().isEmpty()) {
205 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize);
206 sbuf.append(";\" colspan=\"6\" ");
207 sbuf.append("title=\"Mapped Diagnostic Context\">");
208 sbuf.append("MDC: ").append(Transform.escapeHtmlTags(event.getContextData().toMap().toString()));
209 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR);
210 }
211
212 final Throwable throwable = event.getThrown();
213 if (throwable != null) {
214 sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ").append(fontSize);
215 sbuf.append(";\" colspan=\"6\">");
216 appendThrowableAsHtml(throwable, sbuf);
217 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR);
218 }
219
220 return sbuf.toString();
221 }
222
223 @Override
224
225
226
227 public String getContentType() {
228 return contentType;
229 }
230
231 private void appendThrowableAsHtml(final Throwable throwable, final StringBuilder sbuf) {
232 final StringWriter sw = new StringWriter();
233 final PrintWriter pw = new PrintWriter(sw);
234 try {
235 throwable.printStackTrace(pw);
236 } catch (final RuntimeException ex) {
237
238 }
239 pw.flush();
240 final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
241 final ArrayList<String> lines = new ArrayList<>();
242 try {
243 String line = reader.readLine();
244 while (line != null) {
245 lines.add(line);
246 line = reader.readLine();
247 }
248 } catch (final IOException ex) {
249 if (ex instanceof InterruptedIOException) {
250 Thread.currentThread().interrupt();
251 }
252 lines.add(ex.toString());
253 }
254 boolean first = true;
255 for (final String line : lines) {
256 if (!first) {
257 sbuf.append(TRACE_PREFIX);
258 } else {
259 first = false;
260 }
261 sbuf.append(Transform.escapeHtmlTags(line));
262 sbuf.append(Strings.LINE_SEPARATOR);
263 }
264 }
265
266 private StringBuilder appendLs(final StringBuilder sbuilder, final String s) {
267 sbuilder.append(s).append(Strings.LINE_SEPARATOR);
268 return sbuilder;
269 }
270
271 private StringBuilder append(final StringBuilder sbuilder, final String s) {
272 sbuilder.append(s);
273 return sbuilder;
274 }
275
276
277
278
279
280 @Override
281 public byte[] getHeader() {
282 final StringBuilder sbuf = new StringBuilder();
283 append(sbuf, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" ");
284 appendLs(sbuf, "\"http://www.w3.org/TR/html4/loose.dtd\">");
285 appendLs(sbuf, "<html>");
286 appendLs(sbuf, "<head>");
287 append(sbuf, "<meta charset=\"");
288 append(sbuf, getCharset().toString());
289 appendLs(sbuf, "\"/>");
290 append(sbuf, "<title>").append(title);
291 appendLs(sbuf, "</title>");
292 appendLs(sbuf, "<style type=\"text/css\">");
293 appendLs(sbuf, "<!--");
294 append(sbuf, "body, table {font-family:").append(font).append("; font-size: ");
295 appendLs(sbuf, headerSize).append(";}");
296 appendLs(sbuf, "th {background: #336699; color: #FFFFFF; text-align: left;}");
297 appendLs(sbuf, "-->");
298 appendLs(sbuf, "</style>");
299 appendLs(sbuf, "</head>");
300 appendLs(sbuf, "<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">");
301 appendLs(sbuf, "<hr size=\"1\" noshade=\"noshade\">");
302 appendLs(sbuf, "Log session start time " + new java.util.Date() + "<br>");
303 appendLs(sbuf, "<br>");
304 appendLs(sbuf,
305 "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">");
306 appendLs(sbuf, "<tr>");
307 appendLs(sbuf, "<th>Time</th>");
308 appendLs(sbuf, "<th>Thread</th>");
309 appendLs(sbuf, "<th>Level</th>");
310 appendLs(sbuf, "<th>Logger</th>");
311 if (locationInfo) {
312 appendLs(sbuf, "<th>File:Line</th>");
313 }
314 appendLs(sbuf, "<th>Message</th>");
315 appendLs(sbuf, "</tr>");
316 return sbuf.toString().getBytes(getCharset());
317 }
318
319
320
321
322
323 @Override
324 public byte[] getFooter() {
325 final StringBuilder sbuf = new StringBuilder();
326 appendLs(sbuf, "</table>");
327 appendLs(sbuf, "<br>");
328 appendLs(sbuf, "</body></html>");
329 return getBytes(sbuf.toString());
330 }
331
332
333
334
335
336
337
338
339
340
341
342 @PluginFactory
343 public static HtmlLayout createLayout(
344 @PluginAttribute(value = "locationInfo") final boolean locationInfo,
345 @PluginAttribute(value = "title", defaultString = DEFAULT_TITLE) final String title,
346 @PluginAttribute("contentType") String contentType,
347 @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset,
348 @PluginAttribute("fontSize") String fontSize,
349 @PluginAttribute(value = "fontName", defaultString = DEFAULT_FONT_FAMILY) final String font) {
350 final FontSize fs = FontSize.getFontSize(fontSize);
351 fontSize = fs.getFontSize();
352 final String headerSize = fs.larger().getFontSize();
353 if (contentType == null) {
354 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset;
355 }
356 return new HtmlLayout(locationInfo, title, contentType, charset, font, fontSize, headerSize);
357 }
358
359
360
361
362
363
364 public static HtmlLayout createDefaultLayout() {
365 return newBuilder().build();
366 }
367
368 @PluginBuilderFactory
369 public static Builder newBuilder() {
370 return new Builder();
371 }
372
373 public static class Builder implements org.apache.logging.log4j.core.util.Builder<HtmlLayout> {
374
375 @PluginBuilderAttribute
376 private boolean locationInfo = false;
377
378 @PluginBuilderAttribute
379 private String title = DEFAULT_TITLE;
380
381 @PluginBuilderAttribute
382 private String contentType = null;
383
384 @PluginBuilderAttribute
385 private Charset charset = StandardCharsets.UTF_8;
386
387 @PluginBuilderAttribute
388 private FontSize fontSize = FontSize.SMALL;
389
390 @PluginBuilderAttribute
391 private String fontName = DEFAULT_FONT_FAMILY;
392
393 private Builder() {
394 }
395
396 public Builder withLocationInfo(final boolean locationInfo) {
397 this.locationInfo = locationInfo;
398 return this;
399 }
400
401 public Builder withTitle(final String title) {
402 this.title = title;
403 return this;
404 }
405
406 public Builder withContentType(final String contentType) {
407 this.contentType = contentType;
408 return this;
409 }
410
411 public Builder withCharset(final Charset charset) {
412 this.charset = charset;
413 return this;
414 }
415
416 public Builder withFontSize(final FontSize fontSize) {
417 this.fontSize = fontSize;
418 return this;
419 }
420
421 public Builder withFontName(final String fontName) {
422 this.fontName = fontName;
423 return this;
424 }
425
426 @Override
427 public HtmlLayout build() {
428
429 if (contentType == null) {
430 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset;
431 }
432 return new HtmlLayout(locationInfo, title, contentType, charset, fontName, fontSize.getFontSize(),
433 fontSize.larger().getFontSize());
434 }
435 }
436 }