001package ca.uhn.fhir.rest.server.interceptor;
002
003import static org.apache.commons.lang3.StringUtils.isNotBlank;
004
005import java.io.IOException;
006
007/*
008 * #%L
009 * HAPI FHIR - Core Library
010 * %%
011 * Copyright (C) 2014 - 2017 University Health Network
012 * %%
013 * Licensed under the Apache License, Version 2.0 (the "License");
014 * you may not use this file except in compliance with the License.
015 * You may obtain a copy of the License at
016 * 
017 *      http://www.apache.org/licenses/LICENSE-2.0
018 * 
019 * Unless required by applicable law or agreed to in writing, software
020 * distributed under the License is distributed on an "AS IS" BASIS,
021 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
022 * See the License for the specific language governing permissions and
023 * limitations under the License.
024 * #L%
025 */
026
027import java.io.UnsupportedEncodingException;
028import java.net.URLEncoder;
029import java.util.Date;
030import java.util.Map.Entry;
031
032import javax.servlet.ServletException;
033import javax.servlet.http.HttpServletRequest;
034import javax.servlet.http.HttpServletResponse;
035
036import org.apache.commons.lang3.StringUtils;
037import org.apache.commons.lang3.Validate;
038import org.apache.commons.lang3.text.StrLookup;
039import org.apache.commons.lang3.text.StrSubstitutor;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043import ca.uhn.fhir.rest.method.RequestDetails;
044import ca.uhn.fhir.rest.server.Constants;
045import ca.uhn.fhir.rest.server.EncodingEnum;
046import ca.uhn.fhir.rest.server.RestfulServer;
047import ca.uhn.fhir.rest.server.RestfulServerUtils;
048import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
049import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
050import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
051
052/**
053 * Server interceptor which logs each request using a defined format
054 * <p>
055 * The following substitution variables are supported:
056 * </p>
057 * <table summary="Substitution variables supported by this class">
058 * <tr>
059 * <td>${id}</td>
060 * <td>The resource ID associated with this request (or "" if none)</td>
061 * </tr>
062 * <tr>
063 * <td>${idOrResourceName}</td>
064 * <td>The resource ID associated with this request, or the resource name if the request applies to a type but not an
065 * instance, or "" otherwise</td>
066 * </tr>
067 * <tr>
068 * <td>${operationName}</td>
069 * <td>If the request is an extended operation (e.g. "$validate") this value will be the operation name, or ""
070 * otherwise</td>
071 * </tr>
072 * <tr>
073 * <td>${operationType}</td>
074 * <td>A code indicating the operation type for this request, e.g. "read", "history-instance",
075 * "extended-operation-instance", etc.)</td>
076 * </tr>
077 * <tr>
078 * <td>${remoteAddr}</td>
079 * <td>The originating IP of the request</td>
080 * </tr>
081 * <tr>
082 * <td>${requestHeader.XXXX}</td>
083 * <td>The value of the HTTP request header named XXXX. For example, a substitution variable named
084 * "${requestHeader.x-forwarded-for} will yield the value of the first header named "x-forwarded-for
085 * ", or "" if none.</td>
086 * </tr>
087 * <tr>
088 * <td>${requestParameters}</td>
089 * <td>The HTTP request parameters (or "")</td>
090 * </tr>
091 * <tr>
092 * <td>${responseEncodingNoDefault}</td>
093 * <td>The encoding format requested by the client via the _format parameter or the Accept header. Value will be "json"
094 * or "xml", or "" if the client did not explicitly request a format</td>
095 * </tr>
096 * <tr>
097 * <td>${servletPath}</td>
098 * <td>The part of thre requesting URL that corresponds to the particular Servlet being called (see
099 * {@link HttpServletRequest#getServletPath()})</td>
100 * </tr>
101 * <tr>
102 * <td>${requestBodyFhir}</td>
103 * <td>The complete body of the request if the request has a FHIR content-type (this can be quite large!). Will emit an
104 * empty string if the content type is not a FHIR content type</td>
105 * </tr>
106 * <tr>
107 * <td>${requestUrl}</td>
108 * <td>The complete URL of the request</td>
109 * </tr>
110 * <tr>
111 * <td>${requestVerb}</td>
112 * <td>The HTTP verb of the request</td>
113 * </tr>
114 * <tr>
115 * <td>${exceptionMessage}</td>
116 * <td>Applies only to an error message: The message from {@link Exception#getMessage()}</td>
117 * </tr>
118 * </table>
119 */
120
121/*
122 * TODO: implement this, but it needs the logging to happen at the end
123 * <tr>
124 * <td>${processingTimeMillis}</td>
125 * <td>The number of milliseconds spent processing this request</td>
126 * </tr>
127
128 */
129public class LoggingInterceptor extends InterceptorAdapter {
130
131        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class);
132
133        private String myErrorMessageFormat = "ERROR - ${operationType} - ${idOrResourceName}";
134        private boolean myLogExceptions = true;
135        private Logger myLogger = ourLog;
136        private String myMessageFormat = "${operationType} - ${idOrResourceName}";
137
138        /**
139         * Constructor for server logging interceptor
140         */
141        public LoggingInterceptor() {
142                super();
143        }
144        
145        /**
146         * Get the log message format to be used when logging exceptions
147         */
148        public String getErrorMessageFormat() {
149                return myErrorMessageFormat;
150        }
151
152        @Override
153        public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, IOException {
154                if (myLogExceptions) {
155                        // Perform any string substitutions from the message format
156                        StrLookup<?> lookup = new MyLookup(theServletRequest, theException, theRequestDetails);
157                        StrSubstitutor subs = new StrSubstitutor(lookup, "${", "}", '\\');
158
159                        // Actuall log the line
160                        String line = subs.replace(myErrorMessageFormat);
161                        myLogger.info(line);
162
163                }
164                return true;
165        }
166
167
168        @Override
169        public void processingCompletedNormally(ServletRequestDetails theRequestDetails) {
170                // Perform any string substitutions from the message format
171                StrLookup<?> lookup = new MyLookup(theRequestDetails.getServletRequest(), theRequestDetails);
172                StrSubstitutor subs = new StrSubstitutor(lookup, "${", "}", '\\');
173
174                // Actuall log the line
175                String line = subs.replace(myMessageFormat);
176                myLogger.info(line);
177        }
178
179        /**
180         * Should exceptions be logged by this logger
181         */
182        public boolean isLogExceptions() {
183                return myLogExceptions;
184        }
185
186        /**
187         * Set the log message format to be used when logging exceptions
188         */
189        public void setErrorMessageFormat(String theErrorMessageFormat) {
190                Validate.notBlank(theErrorMessageFormat, "Message format can not be null/empty");
191                myErrorMessageFormat = theErrorMessageFormat;
192        }
193
194        /**
195         * Should exceptions be logged by this logger
196         */
197        public void setLogExceptions(boolean theLogExceptions) {
198                myLogExceptions = theLogExceptions;
199        }
200
201        public void setLogger(Logger theLogger) {
202                Validate.notNull(theLogger, "Logger can not be null");
203                myLogger = theLogger;
204        }
205
206        public void setLoggerName(String theLoggerName) {
207                Validate.notBlank(theLoggerName, "Logger name can not be null/empty");
208                myLogger = LoggerFactory.getLogger(theLoggerName);
209
210        }
211
212        /**
213         * Sets the message format itself. See the {@link LoggingInterceptor class documentation} for information on the
214         * format
215         */
216        public void setMessageFormat(String theMessageFormat) {
217                Validate.notBlank(theMessageFormat, "Message format can not be null/empty");
218                myMessageFormat = theMessageFormat;
219        }
220
221        private static final class MyLookup extends StrLookup<String> {
222                private final Throwable myException;
223                private final HttpServletRequest myRequest;
224                private final RequestDetails myRequestDetails;
225
226                private MyLookup(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
227                        myRequest = theRequest;
228                        myRequestDetails = theRequestDetails;
229                        myException = null;
230                }
231
232                public MyLookup(HttpServletRequest theServletRequest, BaseServerResponseException theException, RequestDetails theRequestDetails) {
233                        myException = theException;
234                        myRequestDetails = theRequestDetails;
235                        myRequest = theServletRequest;
236                }
237
238                @Override
239                public String lookup(String theKey) {
240
241                        /*
242                         * TODO: this method could be made more efficient through some sort of lookup map
243                         */
244
245                        if ("operationType".equals(theKey)) {
246                                if (myRequestDetails.getRestOperationType() != null) {
247                                        return myRequestDetails.getRestOperationType().getCode();
248                                }
249                                return "";
250                        } else if ("operationName".equals(theKey)) {
251                                if (myRequestDetails.getRestOperationType() != null) {
252                                        switch (myRequestDetails.getRestOperationType()) {
253                                        case EXTENDED_OPERATION_INSTANCE:
254                                        case EXTENDED_OPERATION_SERVER:
255                                        case EXTENDED_OPERATION_TYPE:
256                                                return myRequestDetails.getOperation();
257                                        default:
258                                                return "";
259                                        }
260                                }
261                                        return "";
262                        } else if ("id".equals(theKey)) {
263                                if (myRequestDetails.getId() != null) {
264                                        return myRequestDetails.getId().getValue();
265                                }
266                                return "";
267                        } else if ("servletPath".equals(theKey)) {
268                                return StringUtils.defaultString(myRequest.getServletPath());
269                        } else if ("idOrResourceName".equals(theKey)) {
270                                if (myRequestDetails.getId() != null) {
271                                        return myRequestDetails.getId().getValue();
272                                }
273                                if (myRequestDetails.getResourceName() != null) {
274                                        return myRequestDetails.getResourceName();
275                                }
276                                return "";
277                        } else if (theKey.equals("requestParameters")) {
278                                StringBuilder b = new StringBuilder();
279                                for (Entry<String, String[]> next : myRequestDetails.getParameters().entrySet()) {
280                                        for (String nextValue : next.getValue()) {
281                                                if (b.length() == 0) {
282                                                        b.append('?');
283                                                } else {
284                                                        b.append('&');
285                                                }
286                                                try {
287                                                        b.append(URLEncoder.encode(next.getKey(), "UTF-8"));
288                                                        b.append('=');
289                                                        b.append(URLEncoder.encode(nextValue, "UTF-8"));
290                                                } catch (UnsupportedEncodingException e) {
291                                                        throw new ca.uhn.fhir.context.ConfigurationException("UTF-8 not supported", e);
292                                                }
293                                        }
294                                }
295                                return b.toString();
296                        } else if (theKey.startsWith("requestHeader.")) {
297                                String val = myRequest.getHeader(theKey.substring("requestHeader.".length()));
298                                return StringUtils.defaultString(val);
299                        } else if (theKey.startsWith("remoteAddr")) {
300                                return StringUtils.defaultString(myRequest.getRemoteAddr());
301                        } else if (theKey.equals("responseEncodingNoDefault")) {
302                                ResponseEncoding encoding = RestfulServerUtils.determineResponseEncodingNoDefault(myRequestDetails, myRequestDetails.getServer().getDefaultResponseEncoding());
303                                if (encoding != null) {
304                                        return encoding.getEncoding().name();
305                                }
306                                return "";
307                        } else if (theKey.equals("exceptionMessage")) {
308                                return myException != null ? myException.getMessage() : null;
309                        } else if (theKey.equals("requestUrl")) {
310                                return myRequest.getRequestURL().toString();
311                        } else if (theKey.equals("requestVerb")) {
312                                return myRequest.getMethod();
313                        } else if (theKey.equals("requestBodyFhir")) {
314                                String contentType = myRequest.getContentType();
315                                if (isNotBlank(contentType)) {
316                                        int colonIndex = contentType.indexOf(';');
317                                        if (colonIndex != -1) {
318                                                contentType = contentType.substring(0, colonIndex);
319                                        }
320                                        contentType = contentType.trim();
321
322                                        EncodingEnum encoding = EncodingEnum.forContentType(contentType);
323                                        if (encoding != null) {
324                                                byte[] requestContents = myRequestDetails.loadRequestContents();
325                                                return new String(requestContents, Constants.CHARSET_UTF8);
326                                        }
327                                }
328                                return "";
329                        } else if ("processingTimeMillis".equals(theKey)) {
330                                Date startTime = (Date) myRequest.getAttribute(RestfulServer.REQUEST_START_TIME);
331                                if (startTime != null) {
332                                        long time = System.currentTimeMillis() - startTime.getTime();
333                                        return Long.toString(time);
334                                }
335                        }
336
337                        return "!VAL!";
338                }
339        }
340
341}