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

rjung 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 5f010ee726 Improve JsonAccessLogValve: support more patterns like for 
headers and attributes.
5f010ee726 is described below

commit 5f010ee7262e91518ec3c20881fc0ffa0647f8b8
Author: Rainer Jung <rainer.j...@kippdata.de>
AuthorDate: Thu Apr 27 10:41:13 2023 +0200

    Improve JsonAccessLogValve: support more patterns like for headers and 
attributes.
    
    Those will be logged as sub objects.
---
 .../apache/catalina/valves/JsonAccessLogValve.java | 115 +++++++++++++++++++--
 webapps/docs/changelog.xml                         |   9 +-
 webapps/docs/config/valve.xml                      |  32 ++++--
 3 files changed, 137 insertions(+), 19 deletions(-)

diff --git a/java/org/apache/catalina/valves/JsonAccessLogValve.java 
b/java/org/apache/catalina/valves/JsonAccessLogValve.java
index c7de7a47f0..2508814a83 100644
--- a/java/org/apache/catalina/valves/JsonAccessLogValve.java
+++ b/java/org/apache/catalina/valves/JsonAccessLogValve.java
@@ -57,6 +57,14 @@ import org.apache.tomcat.util.json.JSONFilter;
  * <li>v: localServerName</li>
  * <li>I: threadName</li>
  * <li>X: connectionStatus</li>
+ * <li>%{xxx}a: remoteAddress-xxx</li>
+ * <li>%{xxx}p: port-xxx</li>
+ * <li>%{xxx}t: time-xxx</li>
+ * <li>%{xxx}c: cookies</li>
+ * <li>%{xxx}i: requestHeaders</li>
+ * <li>%{xxx}o: responseHeaders</li>
+ * <li>%{xxx}r: requestAttributes</li>
+ * <li>%{xxx}s: sessionAttributes</li>
  * </ul>
  * The attribute list is based on
  * 
https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin/parser_apache2.rb#L72
@@ -92,6 +100,18 @@ public class JsonAccessLogValve extends AccessLogValve {
         PATTERNS = Collections.unmodifiableMap(pattern2AttributeName);
     }
 
+    private static final Map<Character, String> SUB_OBJECT_PATTERNS;
+    static {
+        // FIXME: finalize attribute names
+        Map<Character, String> pattern2AttributeName = new HashMap<>();
+        pattern2AttributeName.put(Character.valueOf('c'), "cookies");
+        pattern2AttributeName.put(Character.valueOf('i'), "requestHeaders");
+        pattern2AttributeName.put(Character.valueOf('o'), "responseHeaders");
+        pattern2AttributeName.put(Character.valueOf('r'), "requestAttributes");
+        pattern2AttributeName.put(Character.valueOf('s'), "sessionAttributes");
+        SUB_OBJECT_PATTERNS = 
Collections.unmodifiableMap(pattern2AttributeName);
+    }
+
     /**
      * write any char
      */
@@ -108,8 +128,28 @@ public class JsonAccessLogValve extends AccessLogValve {
         }
     }
 
+    private boolean addSubkeyedItems(ListIterator<AccessLogElement> iterator, 
List<JsonWrappedElement> elements, String patternAttribute) {
+        if (! elements.isEmpty()) {
+            iterator.add(new StringElement("\"" + patternAttribute + "\": {"));
+            for (JsonWrappedElement element: elements) {
+                iterator.add(element);
+                iterator.add(new CharElement(','));
+            }
+            iterator.previous();
+            iterator.remove();
+            iterator.add(new StringElement("},"));
+            return true;
+        }
+        return false;
+    }
+
     @Override
     protected AccessLogElement[] createLogElements() {
+        Map<Character, List<JsonWrappedElement>> subTypeLists = new 
HashMap<>();
+        for (Character pattern: SUB_OBJECT_PATTERNS.keySet()) {
+            subTypeLists.put(pattern, new ArrayList<JsonWrappedElement>());
+        }
+        boolean hasSub = false;
         List<AccessLogElement> logElements = new 
ArrayList<>(Arrays.asList(super.createLogElements()));
         ListIterator<AccessLogElement> lit = logElements.listIterator();
         lit.add(new CharElement('{'));
@@ -120,23 +160,58 @@ public class JsonAccessLogValve extends AccessLogValve {
                 lit.remove();
                 continue;
             }
-            lit.add(new CharElement(','));
+            // Remove items which should be written as
+            // Json objects and add them later in correct order
+            JsonWrappedElement wrappedLogElement = 
(JsonWrappedElement)logElement;
+            AccessLogElement ale = wrappedLogElement.getDelegate();
+            if (ale instanceof HeaderElement) {
+                
subTypeLists.get(Character.valueOf('i')).add(wrappedLogElement);
+                lit.remove();
+            } else if (ale instanceof ResponseHeaderElement) {
+                
subTypeLists.get(Character.valueOf('o')).add(wrappedLogElement);
+                lit.remove();
+            } else if (ale instanceof RequestAttributeElement) {
+                
subTypeLists.get(Character.valueOf('r')).add(wrappedLogElement);
+                lit.remove();
+            } else if (ale instanceof SessionAttributeElement) {
+                
subTypeLists.get(Character.valueOf('s')).add(wrappedLogElement);
+                lit.remove();
+            } else if (ale instanceof CookieElement) {
+                
subTypeLists.get(Character.valueOf('c')).add(wrappedLogElement);
+                lit.remove();
+            } else {
+                // Keep the simple items and add separator
+                lit.add(new CharElement(','));
+            }
+        }
+        // Add back the items that are output as Json objects
+        for (Character pattern: SUB_OBJECT_PATTERNS.keySet()) {
+            if (addSubkeyedItems(lit, subTypeLists.get(pattern), 
SUB_OBJECT_PATTERNS.get(pattern))) {
+                hasSub = true;
+            }
         }
-        // remove last comma again
+        // remove last comma (or possibly "},")
         lit.previous();
         lit.remove();
-        lit.add(new CharElement('}'));
+        // Last item was a sub object, close it
+        if (hasSub) {
+            lit.add(new StringElement("}}"));
+        } else {
+            lit.add(new CharElement('}'));
+        }
         return logElements.toArray(new AccessLogElement[logElements.size()]);
     }
 
+    @Override
+    protected AccessLogElement createAccessLogElement(String name, char 
pattern) {
+        AccessLogElement ale = super.createAccessLogElement(name, pattern);
+        return new JsonWrappedElement(pattern, name, true, ale);
+    }
+
     @Override
     protected AccessLogElement createAccessLogElement(char pattern) {
         AccessLogElement ale = super.createAccessLogElement(pattern);
-        String attributeName = PATTERNS.get(Character.valueOf(pattern));
-        if (attributeName == null) {
-            attributeName = "other-" + new String(JSONFilter.escape(pattern));
-        }
-        return new JsonWrappedElement(attributeName, true, ale);
+        return new JsonWrappedElement(pattern, true, ale);
     }
 
     private static class JsonWrappedElement implements AccessLogElement, 
CachedElement {
@@ -149,10 +224,30 @@ public class JsonAccessLogValve extends AccessLogValve {
             return JSONFilter.escape(nonEscaped);
         }
 
-        JsonWrappedElement(String attributeName, boolean quoteValue, 
AccessLogElement delegate) {
-            this.attributeName = escapeJsonString(attributeName);
+        JsonWrappedElement(char pattern, String key, boolean quoteValue, 
AccessLogElement delegate) {
             this.quoteValue = quoteValue;
             this.delegate = delegate;
+            String patternAttribute = PATTERNS.get(Character.valueOf(pattern));
+            if (patternAttribute == null) {
+                patternAttribute = "other-" + Character.toString(pattern);
+            }
+            if (key != null && ! "".equals(key)) {
+                if 
(SUB_OBJECT_PATTERNS.containsKey(Character.valueOf(pattern))) {
+                    this.attributeName = escapeJsonString(key);
+                } else {
+                    this.attributeName = escapeJsonString(patternAttribute + 
"-" + key);
+                }
+            } else {
+                this.attributeName = escapeJsonString(patternAttribute);
+            }
+        }
+
+        JsonWrappedElement(char pattern, boolean quoteValue, AccessLogElement 
delegate) {
+            this(pattern, null, quoteValue, delegate);
+        }
+
+        public AccessLogElement getDelegate() {
+            return delegate;
         }
 
         @Override
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 7e995115ea..35c48e5a68 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -118,11 +118,16 @@
       </fix>
       <update>
         Change output of vertical tab in <code>AccessLogValve</code> from
-       <code>\v</code> to <code>\u000b</code>. (rjung)
+        <code>\v</code> to <code>\u000b</code>. (rjung)
       </update>
       <update>
         Improve performance of escaping in <code>AccessLogValve</code>
-       roughly by a factor of two. (rjung)
+        roughly by a factor of two. (rjung)
+      </update>
+      <update>
+        Improve <code>JsonAccessLogValve</code>: support more patterns
+        like for headers and attributes. Those will be logged as sub objects.
+        (rjung)
       </update>
     </changelog>
   </subsection>
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index 210abc1dd4..2da2666002 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -544,18 +544,30 @@
     <a href="#Access_Log_Valve">Access Log Valve</a>,
     there are a few differences:
     <ul>
-    <li>requests are logged as JSON objects. Each "%" prefixed pattern
+    <li>requests are logged as JSON objects.</li>
+    <li>each supported "%X" single character pattern
         identifier results in a key value pair in this object.
         See below for the list of keys used for the respective pattern
         identifiers.</li>
+    <li>each pattern identifiers using a subkey of the form 
<code>%{xxx}X</code>
+        where "X" is one of "a", "p" or "t"
+        results in a key value pair of the form "key-xxx".
+        See below for the list of keys used for the respective pattern
+        identifiers.</li>
+    <li>each pattern identifiers using a subkey of the form 
<code>%{xxx}X</code>
+        where "X" is one of "c", "i", "o", "r" or "s"
+        results in a sub object. See below for the key pointing at this
+        sub object. The keys in the sub object are the "xxx" subkeys in the 
pattern.</li>
+    <li>each unsupported "%X" character pattern
+        identifier results in a key value pair using the key "other-X".</li>
     <li>the values logged are the same as the ones logged by
         the standard <a href="#Access_Log_Valve">Access Log Valve</a>
         for the same pattern identifiers.</li>
-    <li>pattern identifiers using a subkey of the form <code>%{subkey}</code>
-        are not yet supported and get silently ignored.</li>
+    <li>any "xxx" subkeys get Json escaped.</li>
     <li>any verbatim text between pattern identifiers gets silently 
ignored.</li>
     </ul>
-    The JSON object keys used for the respective pattern identifiers are the 
following:
+    The JSON object keys used for the pattern identifiers which
+    do not generate a sub object are the following:
     <ul>
     <li><b><code>%a</code></b>: remoteAddr</li>
     <li><b><code>%A</code></b>: localAddr</li>
@@ -580,11 +592,17 @@
     <li><b><code>%v</code></b>: localServerName</li>
     <li><b><code>%X</code></b>: connectionStatus</li>
     </ul>
+    The JSON object keys used for the pattern identifiers which
+    generate a sub object are the following:
+    <ul>
+    <li><b><code>%c</code></b>: cookies</li>
+    <li><b><code>%i</code></b>: requestHeaders</li>
+    <li><b><code>%o</code></b>: responseHeaders</li>
+    <li><b><code>%r</code></b>: requestAttributes</li>
+    <li><b><code>%s</code></b>: sessionAttributes</li>
+    </ul>
     </p>
 
-    <p>Fields using unknown pattern identifiers will be logged with JSON 
object key
-    <code>other-X</code> where <code>X</code> is the unknown identifier.</p>
-
   </subsection>
 
 </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to