001package ca.uhn.fhir.rest.client.interceptor; 002 003/* 004 * #%L 005 * HAPI FHIR - Client Framework 006 * %% 007 * Copyright (C) 2014 - 2019 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import java.io.IOException; 024import java.io.InputStream; 025import java.nio.charset.StandardCharsets; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029 030import ca.uhn.fhir.interceptor.api.Hook; 031import ca.uhn.fhir.interceptor.api.Interceptor; 032import ca.uhn.fhir.interceptor.api.Pointcut; 033import ca.uhn.fhir.model.primitive.IdDt; 034import ca.uhn.fhir.rest.api.Constants; 035import org.apache.commons.io.IOUtils; 036import org.apache.commons.lang3.Validate; 037import org.slf4j.Logger; 038 039import ca.uhn.fhir.rest.client.api.IClientInterceptor; 040import ca.uhn.fhir.rest.client.api.IHttpRequest; 041import ca.uhn.fhir.rest.client.api.IHttpResponse; 042import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 043 044@Interceptor 045public class LoggingInterceptor implements IClientInterceptor { 046 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class); 047 048 private Logger myLog = ourLog; 049 private boolean myLogRequestBody = false; 050 private boolean myLogRequestHeaders = false; 051 private boolean myLogRequestSummary = true; 052 private boolean myLogResponseBody = false; 053 private boolean myLogResponseHeaders = false; 054 private boolean myLogResponseSummary = true; 055 056 /** 057 * Constructor for client logging interceptor 058 */ 059 public LoggingInterceptor() { 060 super(); 061 } 062 063 /** 064 * Constructor for client logging interceptor 065 * 066 * @param theVerbose 067 * If set to true, all logging is enabled 068 */ 069 public LoggingInterceptor(boolean theVerbose) { 070 if (theVerbose) { 071 setLogRequestBody(true); 072 setLogRequestSummary(true); 073 setLogResponseBody(true); 074 setLogResponseSummary(true); 075 setLogRequestHeaders(true); 076 setLogResponseHeaders(true); 077 } 078 } 079 080 @Override 081 @Hook(Pointcut.CLIENT_REQUEST) 082 public void interceptRequest(IHttpRequest theRequest) { 083 if (myLogRequestSummary) { 084 myLog.info("Client request: {}", theRequest); 085 } 086 087 if (myLogRequestHeaders) { 088 StringBuilder b = headersToString(theRequest.getAllHeaders()); 089 myLog.info("Client request headers:\n{}", b.toString()); 090 } 091 092 if (myLogRequestBody) { 093 try { 094 String content = theRequest.getRequestBodyFromStream(); 095 if (content != null) { 096 myLog.info("Client request body:\n{}", content); 097 } 098 } catch (IllegalStateException | IOException e) { 099 myLog.warn("Failed to replay request contents (during logging attempt, actual FHIR call did not fail)", e); 100 } 101 } 102 } 103 104 @Override 105 @Hook(Pointcut.CLIENT_RESPONSE) 106 public void interceptResponse(IHttpResponse theResponse) throws IOException { 107 if (myLogResponseSummary) { 108 String message = "HTTP " + theResponse.getStatus() + " " + theResponse.getStatusInfo(); 109 String respLocation = ""; 110 111 /* 112 * Add response location 113 */ 114 List<String> locationHeaders = theResponse.getHeaders(Constants.HEADER_LOCATION); 115 if (locationHeaders == null || locationHeaders.isEmpty()) { 116 locationHeaders = theResponse.getHeaders(Constants.HEADER_CONTENT_LOCATION); 117 } 118 if (locationHeaders != null && locationHeaders.size() > 0) { 119 String locationValue = locationHeaders.get(0); 120 IdDt locationValueId = new IdDt(locationValue); 121 if (locationValueId.hasBaseUrl() && locationValueId.hasIdPart()) { 122 locationValue = locationValueId.toUnqualified().getValue(); 123 } 124 respLocation = " (" + locationValue + ")"; 125 } 126 127 String timing = " in " + theResponse.getRequestStopWatch().toString(); 128 myLog.info("Client response: {}{}{}", message, respLocation, timing); 129 } 130 131 if (myLogResponseHeaders) { 132 StringBuilder b = headersToString(theResponse.getAllHeaders()); 133 // if (theResponse.getEntity() != null && theResponse.getEntity().getContentEncoding() != null) { 134 // Header next = theResponse.getEntity().getContentEncoding(); 135 // b.append(next.getName() + ": " + next.getValue()); 136 // } 137 // if (theResponse.getEntity() != null && theResponse.getEntity().getContentType() != null) { 138 // Header next = theResponse.getEntity().getContentType(); 139 // b.append(next.getName() + ": " + next.getValue()); 140 // } 141 if (b.length() == 0) { 142 myLog.info("Client response headers: (none)"); 143 } else { 144 myLog.info("Client response headers:\n{}", b.toString()); 145 } 146 } 147 148 if (myLogResponseBody) { 149 theResponse.bufferEntity(); 150 try (InputStream respEntity = theResponse.readEntity()) { 151 if (respEntity != null) { 152 final byte[] bytes; 153 try { 154 bytes = IOUtils.toByteArray(respEntity); 155 } catch (IllegalStateException e) { 156 throw new InternalErrorException(e); 157 } 158 myLog.info("Client response body:\n{}", new String(bytes, StandardCharsets.UTF_8)); 159 } else { 160 myLog.info("Client response body: (none)"); 161 } 162 } 163 } 164 } 165 166 private StringBuilder headersToString(Map<String, List<String>> theHeaders) { 167 StringBuilder b = new StringBuilder(); 168 if (theHeaders != null && !theHeaders.isEmpty()) { 169 Iterator<String> nameEntries = theHeaders.keySet().iterator(); 170 while(nameEntries.hasNext()) { 171 String key = nameEntries.next(); 172 Iterator<String> values = theHeaders.get(key).iterator(); 173 while(values.hasNext()) { 174 String value = values.next(); 175 b.append(key); 176 b.append(": "); 177 b.append(value); 178 if (nameEntries.hasNext() || values.hasNext()) { 179 b.append('\n'); 180 } 181 } 182 } 183 } 184 return b; 185 } 186 187 /** 188 * Sets a logger to use to log messages (default is a logger with this class' name). This can be used to redirect 189 * logs to a differently named logger instead. 190 * 191 * @param theLogger 192 * The logger to use. Must not be null. 193 */ 194 public void setLogger(Logger theLogger) { 195 Validate.notNull(theLogger, "theLogger can not be null"); 196 myLog = theLogger; 197 } 198 199 /** 200 * Should a summary (one line) for each request be logged, containing the URL and other information 201 */ 202 public void setLogRequestBody(boolean theValue) { 203 myLogRequestBody = theValue; 204 } 205 206 /** 207 * Should headers for each request be logged, containing the URL and other information 208 */ 209 public void setLogRequestHeaders(boolean theValue) { 210 myLogRequestHeaders = theValue; 211 } 212 213 /** 214 * Should a summary (one line) for each request be logged, containing the URL and other information 215 */ 216 public void setLogRequestSummary(boolean theValue) { 217 myLogRequestSummary = theValue; 218 } 219 220 /** 221 * Should a summary (one line) for each request be logged, containing the URL and other information 222 */ 223 public void setLogResponseBody(boolean theValue) { 224 myLogResponseBody = theValue; 225 } 226 227 /** 228 * Should headers for each request be logged, containing the URL and other information 229 */ 230 public void setLogResponseHeaders(boolean theValue) { 231 myLogResponseHeaders = theValue; 232 } 233 234 /** 235 * Should a summary (one line) for each request be logged, containing the URL and other information 236 */ 237 public void setLogResponseSummary(boolean theValue) { 238 myLogResponseSummary = theValue; 239 } 240 241}