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 private String addCharsetToContentType(final String contentType) {
128 if (contentType == null) {
129 return DEFAULT_CONTENT_TYPE + "; charset=" + getCharset();
130 }
131 return contentType.contains("charset") ? contentType : contentType + "; charset=" + getCharset();
132 }
133
134
135
136
137
138
139
140 @Override
141 public String toSerializable(final LogEvent event) {
142 final StringBuilder sbuf = getStringBuilder();
143
144 sbuf.append(Strings.LINE_SEPARATOR).append("<tr>").append(Strings.LINE_SEPARATOR);
145
146 sbuf.append("<td>");
147 sbuf.append(event.getTimeMillis() - jvmStartTime);
148 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
149
150 final String escapedThread = Transform.escapeHtmlTags(event.getThreadName());
151 sbuf.append("<td title=\"").append(escapedThread).append(" thread\">");
152 sbuf.append(escapedThread);
153 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
154
155 sbuf.append("<td title=\"Level\">");
156 if (event.getLevel().equals(Level.DEBUG)) {
157 sbuf.append("<font color=\"#339933\">");
158 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
159 sbuf.append("</font>");
160 } else if (event.getLevel().isMoreSpecificThan(Level.WARN)) {
161 sbuf.append("<font color=\"#993300\"><strong>");
162 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
163 sbuf.append("</strong></font>");
164 } else {
165 sbuf.append(Transform.escapeHtmlTags(String.valueOf(event.getLevel())));
166 }
167 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
168
169 String escapedLogger = Transform.escapeHtmlTags(event.getLoggerName());
170 if (Strings.isEmpty(escapedLogger)) {
171 escapedLogger = LoggerConfig.ROOT;
172 }
173 sbuf.append("<td title=\"").append(escapedLogger).append(" logger\">");
174 sbuf.append(escapedLogger);
175 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
176
177 if (locationInfo) {
178 final StackTraceElement element = event.getSource();
179 sbuf.append("<td>");
180 sbuf.append(Transform.escapeHtmlTags(element.getFileName()));
181 sbuf.append(':');
182 sbuf.append(element.getLineNumber());
183 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
184 }
185
186 sbuf.append("<td title=\"Message\">");
187 sbuf.append(Transform.escapeHtmlTags(event.getMessage().getFormattedMessage()).replaceAll(REGEXP, "<br />"));
188 sbuf.append("</td>").append(Strings.LINE_SEPARATOR);
189 sbuf.append("</tr>").append(Strings.LINE_SEPARATOR);
190
191 if (event.getContextStack() != null && !event.getContextStack().isEmpty()) {
192 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize);
193 sbuf.append(";\" colspan=\"6\" ");
194 sbuf.append("title=\"Nested Diagnostic Context\">");
195 sbuf.append("NDC: ").append(Transform.escapeHtmlTags(event.getContextStack().toString()));
196 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR);
197 }
198
199 if (event.getContextData() != null && !event.getContextData().isEmpty()) {
200 sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : ").append(fontSize);
201 sbuf.append(";\" colspan=\"6\" ");
202 sbuf.append("title=\"Mapped Diagnostic Context\">");
203 sbuf.append("MDC: ").append(Transform.escapeHtmlTags(event.getContextData().toMap().toString()));
204 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR);
205 }
206
207 final Throwable throwable = event.getThrown();
208 if (throwable != null) {
209 sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : ").append(fontSize);
210 sbuf.append(";\" colspan=\"6\">");
211 appendThrowableAsHtml(throwable, sbuf);
212 sbuf.append("</td></tr>").append(Strings.LINE_SEPARATOR);
213 }
214
215 return sbuf.toString();
216 }
217
218 @Override
219
220
221
222 public String getContentType() {
223 return contentType;
224 }
225
226 private void appendThrowableAsHtml(final Throwable throwable, final StringBuilder sbuf) {
227 final StringWriter sw = new StringWriter();
228 final PrintWriter pw = new PrintWriter(sw);
229 try {
230 throwable.printStackTrace(pw);
231 } catch (final RuntimeException ex) {
232
233 }
234 pw.flush();
235 final LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
236 final ArrayList<String> lines = new ArrayList<>();
237 try {
238 String line = reader.readLine();
239 while (line != null) {
240 lines.add(line);
241 line = reader.readLine();
242 }
243 } catch (final IOException ex) {
244 if (ex instanceof InterruptedIOException) {
245 Thread.currentThread().interrupt();
246 }
247 lines.add(ex.toString());
248 }
249 boolean first = true;
250 for (final String line : lines) {
251 if (!first) {
252 sbuf.append(TRACE_PREFIX);
253 } else {
254 first = false;
255 }
256 sbuf.append(Transform.escapeHtmlTags(line));
257 sbuf.append(Strings.LINE_SEPARATOR);
258 }
259 }
260
261 private StringBuilder appendLs(final StringBuilder sbuilder, final String s) {
262 sbuilder.append(s).append(Strings.LINE_SEPARATOR);
263 return sbuilder;
264 }
265
266 private StringBuilder append(final StringBuilder sbuilder, final String s) {
267 sbuilder.append(s);
268 return sbuilder;
269 }
270
271
272
273
274
275 @Override
276 public byte[] getHeader() {
277 final StringBuilder sbuf = new StringBuilder();
278 append(sbuf, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" ");
279 appendLs(sbuf, "\"http://www.w3.org/TR/html4/loose.dtd\">");
280 appendLs(sbuf, "<html>");
281 appendLs(sbuf, "<head>");
282 append(sbuf, "<meta charset=\"");
283 append(sbuf, getCharset().toString());
284 appendLs(sbuf, "\"/>");
285 append(sbuf, "<title>").append(title);
286 appendLs(sbuf, "</title>");
287 appendLs(sbuf, "<style type=\"text/css\">");
288 appendLs(sbuf, "<!--");
289 append(sbuf, "body, table {font-family:").append(font).append("; font-size: ");
290 appendLs(sbuf, headerSize).append(";}");
291 appendLs(sbuf, "th {background: #336699; color: #FFFFFF; text-align: left;}");
292 appendLs(sbuf, "-->");
293 appendLs(sbuf, "</style>");
294 appendLs(sbuf, "</head>");
295 appendLs(sbuf, "<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">");
296 appendLs(sbuf, "<hr size=\"1\" noshade=\"noshade\">");
297 appendLs(sbuf, "Log session start time " + new java.util.Date() + "<br>");
298 appendLs(sbuf, "<br>");
299 appendLs(sbuf,
300 "<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">");
301 appendLs(sbuf, "<tr>");
302 appendLs(sbuf, "<th>Time</th>");
303 appendLs(sbuf, "<th>Thread</th>");
304 appendLs(sbuf, "<th>Level</th>");
305 appendLs(sbuf, "<th>Logger</th>");
306 if (locationInfo) {
307 appendLs(sbuf, "<th>File:Line</th>");
308 }
309 appendLs(sbuf, "<th>Message</th>");
310 appendLs(sbuf, "</tr>");
311 return sbuf.toString().getBytes(getCharset());
312 }
313
314
315
316
317
318 @Override
319 public byte[] getFooter() {
320 final StringBuilder sbuf = new StringBuilder();
321 appendLs(sbuf, "</table>");
322 appendLs(sbuf, "<br>");
323 appendLs(sbuf, "</body></html>");
324 return getBytes(sbuf.toString());
325 }
326
327
328
329
330
331
332
333
334
335
336
337 @PluginFactory
338 public static HtmlLayout createLayout(
339 @PluginAttribute(value = "locationInfo") final boolean locationInfo,
340 @PluginAttribute(value = "title", defaultString = DEFAULT_TITLE) final String title,
341 @PluginAttribute("contentType") String contentType,
342 @PluginAttribute(value = "charset", defaultString = "UTF-8") final Charset charset,
343 @PluginAttribute("fontSize") String fontSize,
344 @PluginAttribute(value = "fontName", defaultString = DEFAULT_FONT_FAMILY) final String font) {
345 final FontSize fs = FontSize.getFontSize(fontSize);
346 fontSize = fs.getFontSize();
347 final String headerSize = fs.larger().getFontSize();
348 if (contentType == null) {
349 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset;
350 }
351 return new HtmlLayout(locationInfo, title, contentType, charset, font, fontSize, headerSize);
352 }
353
354
355
356
357
358
359 public static HtmlLayout createDefaultLayout() {
360 return newBuilder().build();
361 }
362
363 @PluginBuilderFactory
364 public static Builder newBuilder() {
365 return new Builder();
366 }
367
368 public static class Builder implements org.apache.logging.log4j.core.util.Builder<HtmlLayout> {
369
370 @PluginBuilderAttribute
371 private boolean locationInfo = false;
372
373 @PluginBuilderAttribute
374 private String title = DEFAULT_TITLE;
375
376 @PluginBuilderAttribute
377 private String contentType = null;
378
379 @PluginBuilderAttribute
380 private Charset charset = StandardCharsets.UTF_8;
381
382 @PluginBuilderAttribute
383 private FontSize fontSize = FontSize.SMALL;
384
385 @PluginBuilderAttribute
386 private String fontName = DEFAULT_FONT_FAMILY;
387
388 private Builder() {
389 }
390
391 public Builder withLocationInfo(final boolean locationInfo) {
392 this.locationInfo = locationInfo;
393 return this;
394 }
395
396 public Builder withTitle(final String title) {
397 this.title = title;
398 return this;
399 }
400
401 public Builder withContentType(final String contentType) {
402 this.contentType = contentType;
403 return this;
404 }
405
406 public Builder withCharset(final Charset charset) {
407 this.charset = charset;
408 return this;
409 }
410
411 public Builder withFontSize(final FontSize fontSize) {
412 this.fontSize = fontSize;
413 return this;
414 }
415
416 public Builder withFontName(final String fontName) {
417 this.fontName = fontName;
418 return this;
419 }
420
421 @Override
422 public HtmlLayout build() {
423
424 if (contentType == null) {
425 contentType = DEFAULT_CONTENT_TYPE + "; charset=" + charset;
426 }
427 return new HtmlLayout(locationInfo, title, contentType, charset, fontName, fontSize.getFontSize(),
428 fontSize.larger().getFontSize());
429 }
430 }
431 }