This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new ce4c29bc36 Add JSON formatter to JULI
ce4c29bc36 is described below
commit ce4c29bc3694e04fc50835fdb16216eb3331584a
Author: remm <[email protected]>
AuthorDate: Sat Feb 8 14:30:00 2025 +0100
Add JSON formatter to JULI
Generates one line JSON, similar to access log.
---
java/org/apache/juli/JsonFormatter.java | 208 ++++++++++++++++++++++++++++
java/org/apache/juli/OneLineFormatter.java | 5 +-
test/org/apache/juli/TestJsonFormatter.java | 59 ++++++++
webapps/docs/changelog.xml | 8 ++
4 files changed, 279 insertions(+), 1 deletion(-)
diff --git a/java/org/apache/juli/JsonFormatter.java
b/java/org/apache/juli/JsonFormatter.java
new file mode 100644
index 0000000000..b0ae7e9576
--- /dev/null
+++ b/java/org/apache/juli/JsonFormatter.java
@@ -0,0 +1,208 @@
+/*
+ * 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.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</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>error: the message from an exception, if present</li>
+ * <li>trace: 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
string,
+ * then moving on to the cause exception if any)</li>
+ * </ul>
+ */
+public class JsonFormatter extends OneLineFormatter {
+
+ @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\": \"");
+ final String threadName = Thread.currentThread().getName();
+ if (threadName != null &&
threadName.startsWith(AsyncFileHandler.THREAD_PREFIX)) {
+ // If using the async handler can't get the thread name from the
+ // current thread.
+ sb.append(getThreadName(record.getThreadID()));
+ } else {
+ sb.append(threadName);
+ }
+ 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("\", ");
+
+ // Error
+ sb.append("\"error\": \"");
+ sb.append(JSONFilter.escape(t.toString()));
+ sb.append("\", ");
+
+ // Stack trace
+ sb.append("\"trace\": [");
+ 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) {
+ switch (c) {
+ case '"':
+ case '\\':
+ case '/':
+ return c;
+ case 0x8:
+ return 'b';
+ case 0xc:
+ return 'f';
+ case 0xa:
+ return 'n';
+ case 0xd:
+ return 'r';
+ case 0x9:
+ return 't';
+ default:
+ return 0;
+ }
+ }
+
+ }
+}
diff --git a/java/org/apache/juli/OneLineFormatter.java
b/java/org/apache/juli/OneLineFormatter.java
index 6f9fd62a21..c48811af7d 100644
--- a/java/org/apache/juli/OneLineFormatter.java
+++ b/java/org/apache/juli/OneLineFormatter.java
@@ -213,8 +213,11 @@ public class OneLineFormatter extends Formatter {
* <p>
* Words fail me to describe what I think of the design decision to use an
int in LogRecord for a long value and the
* resulting mess that follows.
+ *
+ * @param logRecordThreadId the thread id
+ * @return the thread name
*/
- private static String getThreadName(int logRecordThreadId) {
+ protected static String getThreadName(int logRecordThreadId) {
Map<Integer, String> cache = threadNameCache.get();
String result = cache.get(Integer.valueOf(logRecordThreadId));
diff --git a/test/org/apache/juli/TestJsonFormatter.java
b/test/org/apache/juli/TestJsonFormatter.java
new file mode 100644
index 0000000000..9114556ad9
--- /dev/null
+++ b/test/org/apache/juli/TestJsonFormatter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.io.StringReader;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.logging.Formatter;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.tomcat.util.json.JSONParser;
+
+public class TestJsonFormatter {
+
+ @Test
+ public void testFormat() throws Exception {
+
+ Formatter formatter = new JsonFormatter();
+ LogRecord logRecord = new LogRecord(Level.FINE, "Test log message");
+ logRecord.setSourceClassName("org.apache.juli.TestJsonFormatter");
+ logRecord.setSourceMethodName("testFormat");
+ try {
+ throw new IllegalStateException("Bad state");
+ } catch (IllegalStateException e) {
+ logRecord.setThrown(e);
+ }
+
+ String result = formatter.format(logRecord);
+
+ // Verify JSON content
+ Assert.assertTrue(result.startsWith("{"));
+ JSONParser parser = new JSONParser(new StringReader(result));
+ LinkedHashMap<String,Object> json = parser.object();
+ Assert.assertEquals(json.get("method"), "testFormat");
+ @SuppressWarnings("unchecked")
+ ArrayList<Object> trace = (ArrayList<Object>) json.get("trace");
+ Assert.assertEquals(trace.get(0), "java.lang.IllegalStateException:
Bad state");
+
+ }
+
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 0b2757e9cf..8672051464 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -105,6 +105,14 @@
issues do not "pop up" wrt. others).
-->
<section name="Tomcat 9.0.100 (remm)" rtext="in development">
+ <subsection name="Other">
+ <changelog>
+ <add>
+ Add <code>org.apache.juli.JsonFormatter</code> to format log as one
+ line JSON documents. (remm)
+ </add>
+ </changelog>
+ </subsection>
</section>
<section name="Tomcat 9.0.99 (remm)" rtext="release in progress">
<subsection name="Catalina">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]