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