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