View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.filter.logging;
21  
22  import java.net.InetSocketAddress;
23  import java.util.EnumSet;
24  import java.util.HashSet;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.Arrays;
28  import java.util.concurrent.ConcurrentHashMap;
29  
30  import org.apache.mina.core.filterchain.IoFilterEvent;
31  import org.apache.mina.core.session.AttributeKey;
32  import org.apache.mina.core.session.IoSession;
33  import org.apache.mina.filter.util.CommonEventFilter;
34  import org.slf4j.MDC;
35  
36  /**
37   * This filter will inject some key IoSession properties into the Mapped Diagnostic Context (MDC)
38   * <p>
39   * These properties will be set in the MDC for all logging events that are generated
40   * down the call stack, even in code that is not aware of MINA.
41   *
42   * By default, the following properties will be set for all transports:
43   * <ul>
44   *  <li>"handlerClass"</li>
45   *  <li>"remoteAddress"</li>
46   *  <li>"localAddress"</li>
47   * </ul>
48   *
49   * When <code>session.getTransportMetadata().getAddressType() == InetSocketAddress.class</code>
50   * the following properties will also be set:
51   * <ul>
52   * <li>"remoteIp"</li>
53   * <li>"remotePort"</li>
54   * <li>"localIp"</li>
55   * <li>"localPort"</li>
56   * </ul>
57   *
58   * User code can also add custom properties to the context, via {@link #setProperty(IoSession, String, String)}
59   *
60   * If you only want the MDC to be set for the IoHandler code, it's enough to add
61   * one MdcInjectionFilter at the end of the filter chain.
62   *
63   * If you want the MDC to be set for ALL code, you should
64   *   add an MdcInjectionFilter to the start of the chain
65   *   and add that same MdcInjectionFilter instance after EVERY ExecutorFilter in the chain
66   *
67   * Thus it's ok to have one instance of the MdcInjectionFilter and add it multiple times to the chain
68   * but you should avoid adding multiple instances to the chain.
69   *
70   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
71   */
72  
73  public class MdcInjectionFilter extends CommonEventFilter {
74      /**
75       * This enum lists all the possible keys this filter will process 
76       */
77      public enum MdcKey {
78          /** Tha class handling the requests */
79          handlerClass, 
80          
81          /** The remote peer address */
82          remoteAddress, 
83          
84          /** The local address */
85          localAddress, 
86          
87          /** The remote peer IP address */
88          remoteIp, 
89          
90          /** The remote peer port */
91          remotePort, 
92          
93          /** The local IP address */
94          localIp, 
95          
96          /** The local port */
97          localPort
98      }
99  
100     /** key used for storing the context map in the IoSession */
101     private static final AttributeKeyteKey.html#AttributeKey">AttributeKey CONTEXT_KEY = new AttributeKey(MdcInjectionFilter.class, "context");
102 
103     private ThreadLocal<Integer> callDepth = new ThreadLocal<Integer>() {
104         @Override
105         protected Integer initialValue() {
106             return 0;
107         }
108     };
109 
110     private EnumSet<MdcKey> mdcKeys;
111 
112     /**
113      * Use this constructor when you want to specify which keys to add to the MDC.
114      * You could still add custom keys via {@link #setProperty(IoSession, String, String)}
115      * @param keys set of keys that should be added to the MDC
116      *
117      * @see #setProperty(org.apache.mina.core.session.IoSession, String, String)
118      */
119     public MdcInjectionFilter(EnumSet<MdcKey> keys) {
120         this.mdcKeys = keys.clone();
121     }
122 
123     /**
124      * Use this constructor when you want to specify which keys to add to the MDC
125      * You could still add custom keys via {@link #setProperty(IoSession, String, String)}
126      * @param keys list of keys that should be added to the MDC
127      *
128      * @see #setProperty(org.apache.mina.core.session.IoSession, String, String)
129      */
130     public MdcInjectionFilter(MdcKey... keys) {
131         Set<MdcKey> keySet = new HashSet<>(Arrays.asList(keys));
132         this.mdcKeys = EnumSet.copyOf(keySet);
133     }
134 
135     /**
136      * Create a new MdcInjectionFilter instance
137      */
138     public MdcInjectionFilter() {
139         this.mdcKeys = EnumSet.allOf(MdcKey.class);
140     }
141 
142     /**
143      * {@inheritDoc}
144      */
145     @Override
146     protected void filter(IoFilterEvent event) throws Exception {
147         // since this method can potentially call into itself
148         // we need to check the call depth before clearing the MDC
149         int currentCallDepth = callDepth.get();
150         callDepth.set(currentCallDepth + 1);
151         Map<String, String> context = getAndFillContext(event.getSession());
152 
153         if (currentCallDepth == 0) {
154             /* copy context to the MDC when necessary. */
155             for (Map.Entry<String, String> e : context.entrySet()) {
156                 MDC.put(e.getKey(), e.getValue());
157             }
158         }
159 
160         try {
161             /* propagate event down the filter chain */
162             event.fire();
163         } finally {
164             if (currentCallDepth == 0) {
165                 /* remove context from the MDC */
166                 for (String key : context.keySet()) {
167                     MDC.remove(key);
168                 }
169                 callDepth.remove();
170             } else {
171                 callDepth.set(currentCallDepth);
172             }
173         }
174     }
175 
176     private Map<String, String> getAndFillContext(final IoSession session) {
177         Map<String, String> context = getContext(session);
178         if (context.isEmpty()) {
179             fillContext(session, context);
180         }
181         return context;
182     }
183 
184     @SuppressWarnings("unchecked")
185     private static Map<String, String> getContext(final IoSession session) {
186         Map<String, String> context = (Map<String, String>) session.getAttribute(CONTEXT_KEY);
187         
188         if (context == null) {
189             context = new ConcurrentHashMap<>();
190             session.setAttribute(CONTEXT_KEY, context);
191         }
192         return context;
193     }
194 
195     /**
196      * write key properties of the session to the Mapped Diagnostic Context
197      * sub-classes could override this method to map more/other attributes
198      * @param session the session to map
199      * @param context key properties will be added to this map
200      */
201     protected void fillContext(final IoSession session, final Map<String, String> context) {
202         if (mdcKeys.contains(MdcKey.handlerClass)) {
203             context.put(MdcKey.handlerClass.name(), session.getHandler().getClass().getName());
204         }
205         
206         if (mdcKeys.contains(MdcKey.remoteAddress)) {
207             context.put(MdcKey.remoteAddress.name(), session.getRemoteAddress().toString());
208         }
209         
210         if (mdcKeys.contains(MdcKey.localAddress)) {
211             context.put(MdcKey.localAddress.name(), session.getLocalAddress().toString());
212         }
213         
214         if (session.getTransportMetadata().getAddressType() == InetSocketAddress.class) {
215             InetSocketAddress remoteAddress = (InetSocketAddress) session.getRemoteAddress();
216             InetSocketAddress localAddress = (InetSocketAddress) session.getLocalAddress();
217 
218             if (mdcKeys.contains(MdcKey.remoteIp)) {
219                 context.put(MdcKey.remoteIp.name(), remoteAddress.getAddress().getHostAddress());
220             }
221             
222             if (mdcKeys.contains(MdcKey.remotePort)) {
223                 context.put(MdcKey.remotePort.name(), String.valueOf(remoteAddress.getPort()));
224             }
225             
226             if (mdcKeys.contains(MdcKey.localIp)) {
227                 context.put(MdcKey.localIp.name(), localAddress.getAddress().getHostAddress());
228             }
229             
230             if (mdcKeys.contains(MdcKey.localPort)) {
231                 context.put(MdcKey.localPort.name(), String.valueOf(localAddress.getPort()));
232             }
233         }
234     }
235 
236     /**
237      * Get the property associated with a given key
238      * 
239      * @param session The {@link IoSession} 
240      * @param key The key we are looking at
241      * @return The associated property
242      */
243     public static String getProperty(IoSession session, String key) {
244         if (key == null) {
245             throw new IllegalArgumentException("key should not be null");
246         }
247 
248         Map<String, String> context = getContext(session);
249         String answer = context.get(key);
250         
251         if (answer != null) {
252             return answer;
253         }
254 
255         return MDC.get(key);
256     }
257 
258     /**
259      * Add a property to the context for the given session
260      * This property will be added to the MDC for all subsequent events
261      * @param session The session for which you want to set a property
262      * @param key  The name of the property (should not be null)
263      * @param value The value of the property
264      */
265     public static void setProperty(IoSession session, String key, String value) {
266         if (key == null) {
267             throw new IllegalArgumentException("key should not be null");
268         }
269         
270         if (value == null) {
271             removeProperty(session, key);
272         }
273         
274         Map<String, String> context = getContext(session);
275         context.put(key, value);
276         MDC.put(key, value);
277     }
278 
279     /**
280      * Remove a property from the context for the given session
281      * This property will be removed from the MDC for all subsequent events
282      * @param session The session for which you want to remove a property
283      * @param key  The name of the property (should not be null)
284      */
285     public static void removeProperty(IoSession session, String key) {
286         if (key == null) {
287             throw new IllegalArgumentException("key should not be null");
288         }
289         
290         Map<String, String> context = getContext(session);
291         context.remove(key);
292         MDC.remove(key);
293     }
294 }