| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.juli; |
| |
| import java.util.logging.LogManager; |
| import java.util.logging.LogRecord; |
| |
| /** |
| * Provides the same information as the one line format but using JSON formatting. All the information of the LogRecord |
| * is included as a one line JSON document, including the full stack trace of the associated exception if any. |
| * <p> |
| * The LogRecord is mapped as attributes: |
| * <ul> |
| * <li>time: the log record timestamp, with the default format as {@code yyyy-MM-dd'T'HH:mm:ss.SSSX}</li> |
| * <li>level: the log level</li> |
| * <li>thread: the current on which the log occurred</li> |
| * <li>class: the class from which the log originated</li> |
| * <li>method: the method from which the log originated</li> |
| * <li>message: the log message</li> |
| * <li>throwable: the full stack trace from an exception, if present, represented as an array of string (the message |
| * first, then one string per stack trace element prefixed by a whitespace, then moving on to the cause exception if |
| * any)</li> |
| * </ul> |
| */ |
| public class JsonFormatter extends OneLineFormatter { |
| |
| private static final String DEFAULT_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSX"; |
| |
| public JsonFormatter() { |
| String timeFormat = LogManager.getLogManager().getProperty(JsonFormatter.class.getName() + ".timeFormat"); |
| if (timeFormat == null) { |
| timeFormat = DEFAULT_TIME_FORMAT; |
| } |
| setTimeFormat(timeFormat); |
| } |
| |
| @Override |
| public String format(LogRecord record) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append('{'); |
| |
| // Timestamp |
| sb.append("\"time\": \""); |
| addTimestamp(sb, record.getMillis()); |
| sb.append("\", "); |
| |
| // Severity |
| sb.append("\"level\": \""); |
| sb.append(record.getLevel().getLocalizedName()); |
| sb.append("\", "); |
| |
| // Thread |
| sb.append("\"thread\": \""); |
| sb.append(resolveThreadName(record)); |
| sb.append("\", "); |
| |
| // Source |
| sb.append("\"class\": \""); |
| sb.append(record.getSourceClassName()); |
| sb.append("\", "); |
| sb.append("\"method\": \""); |
| sb.append(record.getSourceMethodName()); |
| sb.append("\", "); |
| |
| // Message |
| sb.append("\"message\": \""); |
| sb.append(JSONFilter.escape(formatMessage(record))); |
| |
| Throwable t = record.getThrown(); |
| if (t != null) { |
| sb.append("\", "); |
| |
| // Stack trace |
| sb.append("\"throwable\": ["); |
| boolean first = true; |
| do { |
| if (!first) { |
| sb.append(','); |
| } else { |
| first = false; |
| } |
| sb.append('\"').append(JSONFilter.escape(t.toString())).append('\"'); |
| for (StackTraceElement element : t.getStackTrace()) { |
| sb.append(',').append('\"').append(' ').append(JSONFilter.escape(element.toString())).append('\"'); |
| } |
| t = t.getCause(); |
| } while (t != null); |
| sb.append(']'); |
| } else { |
| sb.append('\"'); |
| } |
| |
| sb.append('}'); |
| // New line for next record |
| sb.append(System.lineSeparator()); |
| |
| return sb.toString(); |
| } |
| |
| |
| /** |
| * Provides escaping of values so they can be included in a JSON document. Escaping is based on the definition of |
| * JSON found in <a href="https://www.rfc-editor.org/rfc/rfc8259.html">RFC 8259</a>. |
| */ |
| public static class JSONFilter { |
| |
| /** |
| * Escape the given string. |
| * |
| * @param input the string |
| * |
| * @return the escaped string |
| */ |
| public static String escape(String input) { |
| return escape(input, 0, input.length()).toString(); |
| } |
| |
| /** |
| * Escape the given char sequence. |
| * |
| * @param input the char sequence |
| * @param off the offset on which escaping will start |
| * @param length the length which should be escaped |
| * |
| * @return the escaped char sequence corresponding to the specified range |
| */ |
| public static CharSequence escape(CharSequence input, int off, int length) { |
| /* |
| * While any character MAY be escaped, only U+0000 to U+001F (control characters), U+0022 (quotation mark) |
| * and U+005C (reverse solidus) MUST be escaped. |
| */ |
| StringBuilder escaped = null; |
| int lastUnescapedStart = off; |
| for (int i = off; i < length; i++) { |
| char c = input.charAt(i); |
| if (c < 0x20 || c == 0x22 || c == 0x5c || Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) { |
| if (escaped == null) { |
| escaped = new StringBuilder(length + 20); |
| } |
| if (lastUnescapedStart < i) { |
| escaped.append(input.subSequence(lastUnescapedStart, i)); |
| } |
| lastUnescapedStart = i + 1; |
| char popular = getPopularChar(c); |
| if (popular > 0) { |
| escaped.append('\\').append(popular); |
| } else { |
| escaped.append("\\u"); |
| escaped.append(String.format("%04X", Integer.valueOf(c))); |
| } |
| } |
| } |
| if (escaped == null) { |
| if (off == 0 && length == input.length()) { |
| return input; |
| } else { |
| return input.subSequence(off, length - off); |
| } |
| } else { |
| if (lastUnescapedStart < length) { |
| escaped.append(input.subSequence(lastUnescapedStart, length)); |
| } |
| return escaped.toString(); |
| } |
| } |
| |
| private JSONFilter() { |
| // Utility class. Hide the default constructor. |
| } |
| |
| private static char getPopularChar(char c) { |
| return switch (c) { |
| case '"', '\\', '/' -> c; |
| case 0x8 -> 'b'; |
| case 0xc -> 'f'; |
| case 0xa -> 'n'; |
| case 0xd -> 'r'; |
| case 0x9 -> 't'; |
| default -> 0; |
| }; |
| } |
| |
| } |
| } |