This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push: new 7642315355 Add an access log valve that uses a json format 7642315355 is described below commit 76423153551f2c3c55cbb53871e95b2018f4bd7a Author: remm <r...@apache.org> 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 ee651c9489..146830d8d8 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -112,6 +112,10 @@ <code>text/javascript</code> as the media type for JavaScript rather than <code>application/javascript</code>. (markt) </fix> + <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 ea30447b0a..da187ebacd 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: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org