001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.filter;
018
019import java.io.IOException;
020import java.io.UnsupportedEncodingException;
021import javax.servlet.FilterChain;
022import javax.servlet.ServletException;
023import javax.servlet.http.HttpServletRequest;
024import javax.servlet.http.HttpServletResponse;
025import javax.servlet.http.HttpSession;
026
027import org.springframework.http.server.ServletServerHttpRequest;
028import org.springframework.util.Assert;
029import org.springframework.util.StringUtils;
030import org.springframework.web.util.ContentCachingRequestWrapper;
031import org.springframework.web.util.WebUtils;
032
033/**
034 * Base class for {@code Filter}s that perform logging operations before and after a request
035 * is processed.
036 *
037 * <p>Subclasses should override the {@code beforeRequest(HttpServletRequest, String)} and
038 * {@code afterRequest(HttpServletRequest, String)} methods to perform the actual logging
039 * around the request.
040 *
041 * <p>Subclasses are passed the message to write to the log in the {@code beforeRequest} and
042 * {@code afterRequest} methods. By default, only the URI of the request is logged. However,
043 * setting the {@code includeQueryString} property to {@code true} will cause the query string
044 * of the request to be included also. The payload (body) of the request can be logged via the
045 * {@code includePayload} flag. Note that this will only log that which is read, which might
046 * not be the entire payload.
047 *
048 * <p>Prefixes and suffixes for the before and after messages can be configured using the
049 * {@code beforeMessagePrefix}, {@code afterMessagePrefix}, {@code beforeMessageSuffix} and
050 * {@code afterMessageSuffix} properties.
051 *
052 * @author Rob Harrop
053 * @author Juergen Hoeller
054 * @author Rossen Stoyanchev
055 * @since 1.2.5
056 * @see #beforeRequest
057 * @see #afterRequest
058 */
059public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter {
060
061        public static final String DEFAULT_BEFORE_MESSAGE_PREFIX = "Before request [";
062
063        public static final String DEFAULT_BEFORE_MESSAGE_SUFFIX = "]";
064
065        public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";
066
067        public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
068
069        private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 50;
070
071
072        private boolean includeQueryString = false;
073
074        private boolean includeClientInfo = false;
075
076        private boolean includeHeaders = false;
077
078        private boolean includePayload = false;
079
080        private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
081
082        private String beforeMessagePrefix = DEFAULT_BEFORE_MESSAGE_PREFIX;
083
084        private String beforeMessageSuffix = DEFAULT_BEFORE_MESSAGE_SUFFIX;
085
086        private String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;
087
088        private String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;
089
090
091        /**
092         * Set whether the query string should be included in the log message.
093         * <p>Should be configured using an {@code <init-param>} for parameter name
094         * "includeQueryString" in the filter definition in {@code web.xml}.
095         */
096        public void setIncludeQueryString(boolean includeQueryString) {
097                this.includeQueryString = includeQueryString;
098        }
099
100        /**
101         * Return whether the query string should be included in the log message.
102         */
103        protected boolean isIncludeQueryString() {
104                return this.includeQueryString;
105        }
106
107        /**
108         * Set whether the client address and session id should be included in the
109         * log message.
110         * <p>Should be configured using an {@code <init-param>} for parameter name
111         * "includeClientInfo" in the filter definition in {@code web.xml}.
112         */
113        public void setIncludeClientInfo(boolean includeClientInfo) {
114                this.includeClientInfo = includeClientInfo;
115        }
116
117        /**
118         * Return whether the client address and session id should be included in the
119         * log message.
120         */
121        protected boolean isIncludeClientInfo() {
122                return this.includeClientInfo;
123        }
124
125        /**
126         * Set whether the request headers should be included in the log message.
127         * <p>Should be configured using an {@code <init-param>} for parameter name
128         * "includeHeaders" in the filter definition in {@code web.xml}.
129         * @since 4.3
130         */
131        public void setIncludeHeaders(boolean includeHeaders) {
132                this.includeHeaders = includeHeaders;
133        }
134
135        /**
136         * Return whether the request headers should be included in the log message.
137         * @since 4.3
138         */
139        protected boolean isIncludeHeaders() {
140                return this.includeHeaders;
141        }
142
143        /**
144         * Set whether the request payload (body) should be included in the log message.
145         * <p>Should be configured using an {@code <init-param>} for parameter name
146         * "includePayload" in the filter definition in {@code web.xml}.
147         * @since 3.0
148         */
149        public void setIncludePayload(boolean includePayload) {
150                this.includePayload = includePayload;
151        }
152
153        /**
154         * Return whether the request payload (body) should be included in the log message.
155         * @since 3.0
156         */
157        protected boolean isIncludePayload() {
158                return this.includePayload;
159        }
160
161        /**
162         * Set the maximum length of the payload body to be included in the log message.
163         * Default is 50 characters.
164         * @since 3.0
165         */
166        public void setMaxPayloadLength(int maxPayloadLength) {
167                Assert.isTrue(maxPayloadLength >= 0, "'maxPayloadLength' should be larger than or equal to 0");
168                this.maxPayloadLength = maxPayloadLength;
169        }
170
171        /**
172         * Return the maximum length of the payload body to be included in the log message.
173         * @since 3.0
174         */
175        protected int getMaxPayloadLength() {
176                return this.maxPayloadLength;
177        }
178
179        /**
180         * Set the value that should be prepended to the log message written
181         * <i>before</i> a request is processed.
182         */
183        public void setBeforeMessagePrefix(String beforeMessagePrefix) {
184                this.beforeMessagePrefix = beforeMessagePrefix;
185        }
186
187        /**
188         * Set the value that should be appended to the log message written
189         * <i>before</i> a request is processed.
190         */
191        public void setBeforeMessageSuffix(String beforeMessageSuffix) {
192                this.beforeMessageSuffix = beforeMessageSuffix;
193        }
194
195        /**
196         * Set the value that should be prepended to the log message written
197         * <i>after</i> a request is processed.
198         */
199        public void setAfterMessagePrefix(String afterMessagePrefix) {
200                this.afterMessagePrefix = afterMessagePrefix;
201        }
202
203        /**
204         * Set the value that should be appended to the log message written
205         * <i>after</i> a request is processed.
206         */
207        public void setAfterMessageSuffix(String afterMessageSuffix) {
208                this.afterMessageSuffix = afterMessageSuffix;
209        }
210
211
212        /**
213         * The default value is "false" so that the filter may log a "before" message
214         * at the start of request processing and an "after" message at the end from
215         * when the last asynchronously dispatched thread is exiting.
216         */
217        @Override
218        protected boolean shouldNotFilterAsyncDispatch() {
219                return false;
220        }
221
222        /**
223         * Forwards the request to the next filter in the chain and delegates down to the subclasses
224         * to perform the actual request logging both before and after the request is processed.
225         * @see #beforeRequest
226         * @see #afterRequest
227         */
228        @Override
229        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
230                        throws ServletException, IOException {
231
232                boolean isFirstRequest = !isAsyncDispatch(request);
233                HttpServletRequest requestToUse = request;
234
235                if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
236                        requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
237                }
238
239                boolean shouldLog = shouldLog(requestToUse);
240                if (shouldLog && isFirstRequest) {
241                        beforeRequest(requestToUse, getBeforeMessage(requestToUse));
242                }
243                try {
244                        filterChain.doFilter(requestToUse, response);
245                }
246                finally {
247                        if (shouldLog && !isAsyncStarted(requestToUse)) {
248                                afterRequest(requestToUse, getAfterMessage(requestToUse));
249                        }
250                }
251        }
252
253        /**
254         * Get the message to write to the log before the request.
255         * @see #createMessage
256         */
257        private String getBeforeMessage(HttpServletRequest request) {
258                return createMessage(request, this.beforeMessagePrefix, this.beforeMessageSuffix);
259        }
260
261        /**
262         * Get the message to write to the log after the request.
263         * @see #createMessage
264         */
265        private String getAfterMessage(HttpServletRequest request) {
266                return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
267        }
268
269        /**
270         * Create a log message for the given request, prefix and suffix.
271         * <p>If {@code includeQueryString} is {@code true}, then the inner part
272         * of the log message will take the form {@code request_uri?query_string};
273         * otherwise the message will simply be of the form {@code request_uri}.
274         * <p>The final message is composed of the inner part as described and
275         * the supplied prefix and suffix.
276         */
277        protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
278                StringBuilder msg = new StringBuilder();
279                msg.append(prefix);
280                msg.append("uri=").append(request.getRequestURI());
281
282                if (isIncludeQueryString()) {
283                        String queryString = request.getQueryString();
284                        if (queryString != null) {
285                                msg.append('?').append(queryString);
286                        }
287                }
288
289                if (isIncludeClientInfo()) {
290                        String client = request.getRemoteAddr();
291                        if (StringUtils.hasLength(client)) {
292                                msg.append(";client=").append(client);
293                        }
294                        HttpSession session = request.getSession(false);
295                        if (session != null) {
296                                msg.append(";session=").append(session.getId());
297                        }
298                        String user = request.getRemoteUser();
299                        if (user != null) {
300                                msg.append(";user=").append(user);
301                        }
302                }
303
304                if (isIncludeHeaders()) {
305                        msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
306                }
307
308                if (isIncludePayload()) {
309                        ContentCachingRequestWrapper wrapper =
310                                        WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
311                        if (wrapper != null) {
312                                byte[] buf = wrapper.getContentAsByteArray();
313                                if (buf.length > 0) {
314                                        int length = Math.min(buf.length, getMaxPayloadLength());
315                                        String payload;
316                                        try {
317                                                payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
318                                        }
319                                        catch (UnsupportedEncodingException ex) {
320                                                payload = "[unknown]";
321                                        }
322                                        msg.append(";payload=").append(payload);
323                                }
324                        }
325                }
326
327                msg.append(suffix);
328                return msg.toString();
329        }
330
331
332        /**
333         * Determine whether to call the {@link #beforeRequest}/{@link #afterRequest}
334         * methods for the current request, i.e. whether logging is currently active
335         * (and the log message is worth building).
336         * <p>The default implementation always returns {@code true}. Subclasses may
337         * override this with a log level check.
338         * @param request current HTTP request
339         * @return {@code true} if the before/after method should get called;
340         * {@code false} otherwise
341         * @since 4.1.5
342         */
343        protected boolean shouldLog(HttpServletRequest request) {
344                return true;
345        }
346
347        /**
348         * Concrete subclasses should implement this method to write a log message
349         * <i>before</i> the request is processed.
350         * @param request current HTTP request
351         * @param message the message to log
352         */
353        protected abstract void beforeRequest(HttpServletRequest request, String message);
354
355        /**
356         * Concrete subclasses should implement this method to write a log message
357         * <i>after</i> the request is processed.
358         * @param request current HTTP request
359         * @param message the message to log
360         */
361        protected abstract void afterRequest(HttpServletRequest request, String message);
362
363}