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