This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 8.5.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/8.5.x by this push:
     new 857286f8e2 Add an access log valve that uses a json format
857286f8e2 is described below

commit 857286f8e23d2914e0f2f2e5e7bc74695024dadf
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 bfabf957df..f5441e15e6 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 fbcbd7ecab..0d6ac1afe2 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -504,6 +504,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

Reply via email to