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}