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