This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new 00edb6d271 Add an access log valve that uses a json format
00edb6d271 is described below
commit 00edb6d271f6ffbe65a01acc377b1930c7354ab0
Author: remm <[email protected]>
AuthorDate: Fri Mar 3 10:40:08 2023 +0100
Add an access log valve that uses a json format
Note: The attribute names are important, and are not final. Please
comment if you care about this.
There's an inconsistency in the code between JSON and Json. I kept Json
to go along with the existing JsonErrorReportValve.
Based on code submitted by Thomas Meyer in PR#539.
---
.../apache/catalina/valves/JsonAccessLogValve.java | 203 +++++++++++++++++++++
java/org/apache/tomcat/util/json/JSONFilter.java | 6 +-
.../apache/tomcat/util/json/TestJSONFilter.java | 1 +
webapps/docs/changelog.xml | 4 +
webapps/docs/config/valve.xml | 65 +++++++
5 files changed, 277 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/catalina/valves/JsonAccessLogValve.java
b/java/org/apache/catalina/valves/JsonAccessLogValve.java
new file mode 100644
index 0000000000..cd48cfceb2
--- /dev/null
+++ b/java/org/apache/catalina/valves/JsonAccessLogValve.java
@@ -0,0 +1,203 @@
+/*
+ * 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.catalina.valves;
+
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.tomcat.util.json.JSONFilter;
+
+/**
+ * Access log valve derivative that rewrites entries as JSON.
+ * <b>Important note: the attribute names are not final</b>
+ * Patterns are mapped to attributes as followed:
+ * <ul>
+ * <li>a: remoteAddr</li>
+ * <li>A: localAddr</li>
+ * <li>b: size (byteSent: size)</li>
+ * <li>B: byteSentNC</li>
+ * <li>D: elapsedTime</li>
+ * <li>F: firstByteTime</li>
+ * <li>h: host</li>
+ * <li>H: protocol</li>
+ * <li>l: logicalUserName</li>
+ * <li>m: method</li>
+ * <li>p: port</li>
+ * <li>q: query</li>
+ * <li>r: request</li>
+ * <li>s: statusCode</li>
+ * <li>S: sessionId</li>
+ * <li>t: time (dateTime: time)</li>
+ * <li>T: elapsedTimeS</li>
+ * <li>u: user</li>
+ * <li>U: path (requestURI: path)</li>
+ * <li>v: localServerName</li>
+ * <li>I: threadName</li>
+ * <li>X: connectionStatus</li>
+ * </ul>
+ * The attribute list is based on
+ *
https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin/parser_apache2.rb#L72
+ */
+public class JsonAccessLogValve extends AccessLogValve {
+
+ private static final Map<Character, String> PATTERNS;
+ static {
+ // FIXME: finalize attribute names
+ Map<Character, String> pattern2AttributeName = new HashMap<>();
+ pattern2AttributeName.put('a', "remoteAddr");
+ pattern2AttributeName.put('A', "localAddr");
+ pattern2AttributeName.put('b', "size"); /* byteSent -> size */
+ pattern2AttributeName.put('B', "byteSentNC");
+ pattern2AttributeName.put('D', "elapsedTime");
+ pattern2AttributeName.put('F', "firstByteTime");
+ pattern2AttributeName.put('h', "host");
+ pattern2AttributeName.put('H', "protocol");
+ pattern2AttributeName.put('l', "logicalUserName");
+ pattern2AttributeName.put('m', "method");
+ pattern2AttributeName.put('p', "port");
+ pattern2AttributeName.put('q', "query");
+ pattern2AttributeName.put('r', "request");
+ pattern2AttributeName.put('s', "statusCode");
+ pattern2AttributeName.put('S', "sessionId");
+ pattern2AttributeName.put('t', "time"); /* dateTime -> time */
+ pattern2AttributeName.put('T', "elapsedTimeS");
+ pattern2AttributeName.put('u', "user");
+ pattern2AttributeName.put('U', "path"); /* requestURI -> path */
+ pattern2AttributeName.put('v', "localServerName");
+ pattern2AttributeName.put('I', "threadName");
+ pattern2AttributeName.put('X', "connectionStatus");
+ PATTERNS = Collections.unmodifiableMap(pattern2AttributeName);
+ }
+
+ @Override
+ protected AccessLogElement[] createLogElements() {
+ List<AccessLogElement> logElements = new
ArrayList<>(Arrays.asList(super.createLogElements()));
+ ListIterator<AccessLogElement> lit = logElements.listIterator();
+ lit.add((buf, date, req, resp, time) -> buf.write('{'));
+ while (lit.hasNext()) {
+ AccessLogElement logElement = lit.next();
+ // remove all other elements, like StringElements
+ if (!(logElement instanceof JsonWrappedElement)) {
+ lit.remove();
+ continue;
+ }
+ lit.add((buf, date, req, resp, time) -> buf.write(','));
+ }
+ // remove last comma again
+ lit.previous();
+ lit.remove();
+ lit.add((buf, date, req, resp, time) -> buf.write('}'));
+ return logElements.toArray(new AccessLogElement[logElements.size()]);
+ }
+
+ @Override
+ protected AccessLogElement createAccessLogElement(char pattern) {
+ AccessLogElement ale = super.createAccessLogElement(pattern);
+ String attributeName = PATTERNS.get(pattern);
+ if (attributeName == null) {
+ attributeName = "other-" + new String(JSONFilter.escape(pattern));
+ }
+ return new JsonWrappedElement(attributeName, true, ale);
+ }
+
+ /**
+ * JSON string escaping writer
+ */
+ private static class JsonCharArrayWriter extends CharArrayWriter {
+
+ JsonCharArrayWriter(int i) {
+ super(i);
+ }
+
+ @Override
+ public void write(int c) {
+ try {
+ super.write(JSONFilter.escape((char) c));
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void write(char[] c, int off, int len) {
+ try {
+ super.write(JSONFilter.escape(new String(c, off, len)));
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void write(String str, int off, int len) {
+ CharSequence escaped = JSONFilter.escape(str, off, len);
+ super.write(escaped.toString(), 0, escaped.length());
+ }
+ }
+
+ private static class JsonWrappedElement implements AccessLogElement,
CachedElement {
+
+ private CharSequence attributeName;
+ private boolean quoteValue;
+ private AccessLogElement delegate;
+
+ private CharSequence escapeJsonString(CharSequence nonEscaped) {
+ return JSONFilter.escape(nonEscaped);
+ }
+
+ JsonWrappedElement(String attributeName, boolean quoteValue,
AccessLogElement delegate) {
+ this.attributeName = escapeJsonString(attributeName);
+ this.quoteValue = quoteValue;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void addElement(CharArrayWriter buf, Date date, Request
request, Response response, long time) {
+ buf.append('"').append(attributeName).append('"').append(':');
+ if (quoteValue) {
+ buf.append('"');
+ }
+ CharArrayWriter valueWriter = new JsonCharArrayWriter(8);
+ try {
+ delegate.addElement(valueWriter, date, request, response,
time);
+ valueWriter.writeTo(buf);
+ } catch (IOException e) {
+ // ignore
+ }
+ if (quoteValue) {
+ buf.append('"');
+ }
+ }
+
+ @Override
+ public void cache(Request request) {
+ if (delegate instanceof CachedElement) {
+ ((CachedElement) delegate).cache(request);
+ }
+ }
+ }
+
+}
diff --git a/java/org/apache/tomcat/util/json/JSONFilter.java
b/java/org/apache/tomcat/util/json/JSONFilter.java
index fa04275eb4..43c23f331b 100644
--- a/java/org/apache/tomcat/util/json/JSONFilter.java
+++ b/java/org/apache/tomcat/util/json/JSONFilter.java
@@ -29,7 +29,8 @@ public class JSONFilter {
* @return a char array with the escaped sequence
*/
public static char[] escape(char c) {
- if (c < 0x20 || c == 0x22 || c == 0x5c) {
+ if (c < 0x20 || c == 0x22 || c == 0x5c
+ || Character.isHighSurrogate((char) c) ||
Character.isLowSurrogate((char) c)) {
char popular = getPopularChar(c);
if (popular > 0) {
return new char[] { '\\', popular };
@@ -81,7 +82,8 @@ public class JSONFilter {
int lastUnescapedStart = off;
for (int i = off; i < length; i++) {
char c = input.charAt(i);
- if (c < 0x20 || c == 0x22 || c == 0x5c) {
+ if (c < 0x20 || c == 0x22 || c == 0x5c
+ || Character.isHighSurrogate((char) c) ||
Character.isLowSurrogate((char) c)) {
if (escaped == null) {
escaped = new StringBuilder(length + 20);
}
diff --git a/test/org/apache/tomcat/util/json/TestJSONFilter.java
b/test/org/apache/tomcat/util/json/TestJSONFilter.java
index 136f2f1919..58dddeb8fa 100644
--- a/test/org/apache/tomcat/util/json/TestJSONFilter.java
+++ b/test/org/apache/tomcat/util/json/TestJSONFilter.java
@@ -54,6 +54,7 @@ public class TestJSONFilter {
// Start
parameterSets.add(new String[] { "\naaa", "\\naaa" });
parameterSets.add(new String[] { "\n\naaa", "\\n\\naaa" });
+ parameterSets.add(new String[] { "/aaa", "/aaa" });
// Middle
parameterSets.add(new String[] { "aaa\naaa", "aaa\\naaa" });
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 232e9c8f5f..7a9fe57314 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -122,6 +122,10 @@
property when starting. Users are now free to configure this property
if
they wish. (markt)
</scode>
+ <add>
+ Add an access log valve that uses a json format. Based on pull request
+ <pr>539</pr> provided by Thomas Meyer. (remm)
+ </add>
</changelog>
</subsection>
<subsection name="Coyote">
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index 53b73e04da..fc27e20c40 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -503,6 +503,71 @@
</subsection>
+<subsection name="JSON Access Log Valve">
+
+ <subsection name="Introduction">
+
+ <p>The <strong>JSON Access Log Valve</strong> extends the
+ <a href="#Access_Log_Valve">Access Log Valve</a> class, and so
+ uses the same self-contained logging logic. This means it
+ implements many of the same file handling attributes. The main
+ difference to the standard <code>AccessLogValve</code> is that
+ <code>JsonAccessLogValve</code> creates log files which
+ follow the JSON syntax as defined by
+ <a href="https://www.rfc-editor.org/rfc/rfc8259.html">RFC 8259</a>.</p>
+
+ </subsection>
+
+ <subsection name="Attributes">
+
+ <p>The <strong>JSON Access Log Valve</strong> supports all
+ configuration attributes of the standard
+ <a href="#Access_Log_Valve">Access Log Valve.</a> Only the
+ values used for <code>className</code> differ.</p>
+
+ <attributes>
+
+ <attribute name="className" required="true">
+ <p>Java class name of the implementation to use. This MUST be set to
+ <strong>org.apache.catalina.valves.JsonAccessLogValve</strong> to
+ use the extended access log valve.</p>
+ </attribute>
+
+ </attributes>
+
+ <p>While the pattern supported are the same as for the regular access log,
+ those are mapped to specific JSON attribute names. The attributes are the
+ following:
+ <ul>
+ <li>a: remoteAddr</li>
+ <li>A: localAddr</li>
+ <li>b: size (byteSent: size)</li>
+ <li>B: byteSentNC</li>
+ <li>D: elapsedTime</li>
+ <li>F: firstByteTime</li>
+ <li>h: host</li>
+ <li>H: protocol</li>
+ <li>l: logicalUserName</li>
+ <li>m: method</li>
+ <li>p: port</li>
+ <li>q: query</li>
+ <li>r: request</li>
+ <li>s: statusCode</li>
+ <li>S: sessionId</li>
+ <li>t: time (dateTime: time)</li>
+ <li>T: elapsedTimeS</li>
+ <li>u: user</li>
+ <li>U: path (requestURI: path)</li>
+ <li>v: localServerName</li>
+ <li>I: threadName</li>
+ <li>X: connectionStatus</li>
+ </ul>
+ </p>
+
+ </subsection>
+
+</subsection>
+
</section>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]