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