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