001/* 002 * #%L 003 * HAPI FHIR - Client Framework 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.client.interceptor; 021 022import ca.uhn.fhir.i18n.Msg; 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.debug("Client request body:\n{}", content); 095 } 096 } catch (IllegalStateException | IOException e) { 097 myLog.warn( 098 "Failed to replay request contents (during logging attempt, actual FHIR call did not fail)", e); 099 } 100 } 101 } 102 103 @Override 104 @Hook(value = Pointcut.CLIENT_RESPONSE, order = InterceptorOrders.LOGGING_INTERCEPTOR_REQUEST) 105 public void interceptResponse(IHttpResponse theResponse) throws IOException { 106 if (myLogResponseSummary) { 107 String message = "HTTP " + theResponse.getStatus() + " " + theResponse.getStatusInfo(); 108 String respLocation = ""; 109 110 /* 111 * Add response location 112 */ 113 List<String> locationHeaders = theResponse.getHeaders(Constants.HEADER_LOCATION); 114 if (locationHeaders == null || locationHeaders.isEmpty()) { 115 locationHeaders = theResponse.getHeaders(Constants.HEADER_CONTENT_LOCATION); 116 } 117 if (locationHeaders != null && locationHeaders.size() > 0) { 118 String locationValue = locationHeaders.get(0); 119 IdDt locationValueId = new IdDt(locationValue); 120 if (locationValueId.hasBaseUrl() && locationValueId.hasIdPart()) { 121 locationValue = locationValueId.toUnqualified().getValue(); 122 } 123 respLocation = " (" + locationValue + ")"; 124 } 125 126 String timing = " in " + theResponse.getRequestStopWatch().toString(); 127 myLog.info("Client response: {}{}{}", message, respLocation, timing); 128 } 129 130 if (myLogResponseHeaders) { 131 StringBuilder b = headersToString(theResponse.getAllHeaders()); 132 // if (theResponse.getEntity() != null && theResponse.getEntity().getContentEncoding() != null) { 133 // Header next = theResponse.getEntity().getContentEncoding(); 134 // b.append(next.getName() + ": " + next.getValue()); 135 // } 136 // if (theResponse.getEntity() != null && theResponse.getEntity().getContentType() != null) { 137 // Header next = theResponse.getEntity().getContentType(); 138 // b.append(next.getName() + ": " + next.getValue()); 139 // } 140 if (b.length() == 0) { 141 myLog.info("Client response headers: (none)"); 142 } else { 143 myLog.info("Client response headers:\n{}", b.toString()); 144 } 145 } 146 147 if (myLogResponseBody) { 148 theResponse.bufferEntity(); 149 try (InputStream respEntity = theResponse.readEntity()) { 150 if (respEntity != null) { 151 final byte[] bytes; 152 try { 153 bytes = IOUtils.toByteArray(respEntity); 154 } catch (IllegalStateException e) { 155 throw new InternalErrorException(Msg.code(1405) + e); 156 } 157 myLog.debug("Client response body:\n{}", new String(bytes, StandardCharsets.UTF_8)); 158 } else { 159 myLog.info("Client response body: (none)"); 160 } 161 } 162 } 163 } 164 165 private StringBuilder headersToString(Map<String, List<String>> theHeaders) { 166 StringBuilder b = new StringBuilder(); 167 if (theHeaders != null && !theHeaders.isEmpty()) { 168 Iterator<String> nameEntries = theHeaders.keySet().iterator(); 169 while (nameEntries.hasNext()) { 170 String key = nameEntries.next(); 171 Iterator<String> values = theHeaders.get(key).iterator(); 172 while (values.hasNext()) { 173 String value = values.next(); 174 b.append(key); 175 b.append(": "); 176 b.append(value); 177 if (nameEntries.hasNext() || values.hasNext()) { 178 b.append('\n'); 179 } 180 } 181 } 182 } 183 return b; 184 } 185 186 /** 187 * Sets a logger to use to log messages (default is a logger with this class' name). This can be used to redirect 188 * logs to a differently named logger instead. 189 * 190 * @param theLogger The logger to use. Must not be null. 191 */ 192 public void setLogger(Logger theLogger) { 193 Validate.notNull(theLogger, "theLogger can not be null"); 194 myLog = theLogger; 195 } 196 197 /** 198 * Should a summary (one line) for each request be logged, containing the URL and other information 199 */ 200 public LoggingInterceptor setLogRequestBody(boolean theValue) { 201 myLogRequestBody = theValue; 202 return this; 203 } 204 205 /** 206 * Should headers for each request be logged, containing the URL and other information 207 */ 208 public LoggingInterceptor setLogRequestHeaders(boolean theValue) { 209 myLogRequestHeaders = theValue; 210 return this; 211 } 212 213 /** 214 * Should a summary (one line) for each request be logged, containing the URL and other information 215 */ 216 public LoggingInterceptor setLogRequestSummary(boolean theValue) { 217 myLogRequestSummary = theValue; 218 return this; 219 } 220 221 /** 222 * Should a summary (one line) for each request be logged, containing the URL and other information 223 */ 224 public LoggingInterceptor setLogResponseBody(boolean theValue) { 225 myLogResponseBody = theValue; 226 return this; 227 } 228 229 /** 230 * Should headers for each request be logged, containing the URL and other information 231 */ 232 public LoggingInterceptor setLogResponseHeaders(boolean theValue) { 233 myLogResponseHeaders = theValue; 234 return this; 235 } 236 237 /** 238 * Should a summary (one line) for each request be logged, containing the URL and other information 239 */ 240 public LoggingInterceptor setLogResponseSummary(boolean theValue) { 241 myLogResponseSummary = theValue; 242 return this; 243 } 244}