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