| /* |
| * 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.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| 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. |
| * <p> |
| * <b>Important note: the attribute names are not final.</b> |
| * <p> |
| * 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> |
| * <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> |
| * <li>%{xxx}L: identifier</li> |
| * </ul> |
| * The attribute list is based on |
| * <a href="https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin/parser_apache2.rb#L72">parser_apache2.rb</a> |
| */ |
| public class JsonAccessLogValve extends AccessLogValve { |
| |
| /** |
| * Constructs a new JsonAccessLogValve. |
| */ |
| public JsonAccessLogValve() { |
| } |
| |
| private static final Map<Character,String> PATTERNS; |
| static { |
| Map<Character,String> pattern2AttributeName = new HashMap<>(); |
| pattern2AttributeName.put(Character.valueOf('a'), "remoteAddr"); |
| pattern2AttributeName.put(Character.valueOf('A'), "localAddr"); |
| pattern2AttributeName.put(Character.valueOf('b'), "size"); |
| pattern2AttributeName.put(Character.valueOf('B'), "byteSentNC"); |
| pattern2AttributeName.put(Character.valueOf('D'), "elapsedTime"); |
| pattern2AttributeName.put(Character.valueOf('F'), "firstByteTime"); |
| pattern2AttributeName.put(Character.valueOf('h'), "host"); |
| pattern2AttributeName.put(Character.valueOf('H'), "protocol"); |
| pattern2AttributeName.put(Character.valueOf('I'), "threadName"); |
| pattern2AttributeName.put(Character.valueOf('l'), "logicalUserName"); |
| pattern2AttributeName.put(Character.valueOf('m'), "method"); |
| pattern2AttributeName.put(Character.valueOf('p'), "port"); |
| pattern2AttributeName.put(Character.valueOf('q'), "query"); |
| pattern2AttributeName.put(Character.valueOf('r'), "request"); |
| pattern2AttributeName.put(Character.valueOf('s'), "statusCode"); |
| pattern2AttributeName.put(Character.valueOf('S'), "sessionId"); |
| pattern2AttributeName.put(Character.valueOf('t'), "time"); |
| pattern2AttributeName.put(Character.valueOf('T'), "elapsedTimeS"); |
| pattern2AttributeName.put(Character.valueOf('u'), "user"); |
| pattern2AttributeName.put(Character.valueOf('U'), "path"); |
| pattern2AttributeName.put(Character.valueOf('v'), "localServerName"); |
| pattern2AttributeName.put(Character.valueOf('X'), "connectionStatus"); |
| PATTERNS = Collections.unmodifiableMap(pattern2AttributeName); |
| } |
| |
| private static final Map<Character,String> SUB_OBJECT_PATTERNS; |
| static { |
| 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"); |
| pattern2AttributeName.put(Character.valueOf('L'), "identifier"); |
| SUB_OBJECT_PATTERNS = Collections.unmodifiableMap(pattern2AttributeName); |
| } |
| |
| /** |
| * write any char |
| */ |
| protected static class CharElement implements AccessLogElement { |
| private final char ch; |
| |
| /** |
| * Constructs a new CharElement. |
| * |
| * @param ch the character to write |
| */ |
| public CharElement(char ch) { |
| this.ch = ch; |
| } |
| |
| @Override |
| public void addElement(CharArrayWriter buf, Request request, Response response, long time) { |
| buf.write(ch); |
| } |
| } |
| |
| 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<>()); |
| } |
| boolean hasSimple = false; |
| boolean hasSub = false; |
| List<AccessLogElement> logElements = new ArrayList<>(Arrays.asList(super.createLogElements())); |
| ListIterator<AccessLogElement> lit = logElements.listIterator(); |
| lit.add(new CharElement('{')); |
| while (lit.hasNext()) { |
| AccessLogElement logElement = lit.next(); |
| // remove all other elements, like StringElements |
| if (!(logElement instanceof JsonWrappedElement wrappedLogElement)) { |
| lit.remove(); |
| continue; |
| } |
| AccessLogElement ale = wrappedLogElement.getDelegate(); |
| // Remove items which should be written as |
| // Json objects and add them later in correct order |
| 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 if (ale instanceof IdentifierElement) { |
| subTypeLists.get(Character.valueOf('L')).add(wrappedLogElement); |
| lit.remove(); |
| } else { |
| // Keep the simple items and add separator |
| hasSimple = true; |
| 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; |
| } |
| } |
| if (hasSimple || hasSub) { |
| // remove last comma (or possibly "},") |
| lit.previous(); |
| lit.remove(); |
| } |
| // Add closing } |
| if (hasSub) { |
| // Last item was a sub object, close it as well |
| lit.add(new StringElement("}}")); |
| } else { |
| lit.add(new CharElement('}')); |
| } |
| return logElements.toArray(new AccessLogElement[0]); |
| } |
| |
| @Override |
| protected AccessLogElement createAccessLogElement(String name, char pattern) { |
| AccessLogElement ale = super.createAccessLogElement(name, pattern); |
| if ('%' == pattern) { |
| // Uses a pattern but is literal text so no need to wrap. |
| return ale; |
| } else { |
| return new JsonWrappedElement(pattern, name, true, ale); |
| } |
| } |
| |
| @Override |
| protected AccessLogElement createAccessLogElement(char pattern) { |
| AccessLogElement ale = super.createAccessLogElement(pattern); |
| if ('%' == pattern) { |
| // Uses a pattern but is literal text so no need to wrap. |
| return ale; |
| } else { |
| return new JsonWrappedElement(pattern, true, ale); |
| } |
| } |
| |
| private static class JsonWrappedElement implements AccessLogElement, CachedElement { |
| |
| private final CharSequence attributeName; |
| private final boolean quoteValue; |
| private final AccessLogElement delegate; |
| |
| private CharSequence escapeJsonString(CharSequence nonEscaped) { |
| return JSONFilter.escape(nonEscaped); |
| } |
| |
| 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 && !key.isEmpty()) { |
| 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 |
| public void addElement(CharArrayWriter buf, Request request, Response response, long time) { |
| buf.append('"').append(attributeName).append('"').append(':'); |
| if (quoteValue) { |
| buf.append('"'); |
| } |
| // The value will already be log escaped |
| delegate.addElement(buf, request, response, time); |
| if (quoteValue) { |
| buf.append('"'); |
| } |
| } |
| |
| @Override |
| public void cache(Request request) { |
| if (delegate instanceof CachedElement) { |
| ((CachedElement) delegate).cache(request); |
| } |
| } |
| } |
| |
| } |