
001package ca.uhn.fhir.rest.client.interceptor; 002 003/* 004 * #%L 005 * HAPI FHIR - Client Framework 006 * %% 007 * Copyright (C) 2014 - 2023 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.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.Hook; 025import ca.uhn.fhir.interceptor.api.Interceptor; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.model.primitive.IdDt; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.client.api.IClientInterceptor; 030import ca.uhn.fhir.rest.client.api.IHttpRequest; 031import ca.uhn.fhir.rest.client.api.IHttpResponse; 032import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 033import org.apache.commons.io.IOUtils; 034import org.apache.commons.lang3.Validate; 035import org.slf4j.Logger; 036 037import java.io.IOException; 038import java.io.InputStream; 039import java.nio.charset.StandardCharsets; 040import java.util.Iterator; 041import java.util.List; 042import java.util.Map; 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 If set to true, all logging is enabled 067 */ 068 public LoggingInterceptor(boolean theVerbose) { 069 if (theVerbose) { 070 setLogRequestBody(true); 071 setLogRequestSummary(true); 072 setLogResponseBody(true); 073 setLogResponseSummary(true); 074 setLogRequestHeaders(true); 075 setLogResponseHeaders(true); 076 } 077 } 078 079 @Override 080 @Hook(value = Pointcut.CLIENT_REQUEST, order = InterceptorOrders.LOGGING_INTERCEPTOR_RESPONSE) 081 public void interceptRequest(IHttpRequest theRequest) { 082 if (myLogRequestSummary) { 083 myLog.info("Client request: {}", theRequest); 084 } 085 086 if (myLogRequestHeaders) { 087 StringBuilder b = headersToString(theRequest.getAllHeaders()); 088 myLog.info("Client request headers:\n{}", b.toString()); 089 } 090 091 if (myLogRequestBody) { 092 try { 093 String content = theRequest.getRequestBodyFromStream(); 094 if (content != null) { 095 myLog.debug("Client request body:\n{}", content); 096 } 097 } catch (IllegalStateException | IOException e) { 098 myLog.warn("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 void setLogRequestBody(boolean theValue) { 201 myLogRequestBody = theValue; 202 } 203 204 /** 205 * Should headers for each request be logged, containing the URL and other information 206 */ 207 public void setLogRequestHeaders(boolean theValue) { 208 myLogRequestHeaders = theValue; 209 } 210 211 /** 212 * Should a summary (one line) for each request be logged, containing the URL and other information 213 */ 214 public void setLogRequestSummary(boolean theValue) { 215 myLogRequestSummary = theValue; 216 } 217 218 /** 219 * Should a summary (one line) for each request be logged, containing the URL and other information 220 */ 221 public void setLogResponseBody(boolean theValue) { 222 myLogResponseBody = theValue; 223 } 224 225 /** 226 * Should headers for each request be logged, containing the URL and other information 227 */ 228 public void setLogResponseHeaders(boolean theValue) { 229 myLogResponseHeaders = theValue; 230 } 231 232 /** 233 * Should a summary (one line) for each request be logged, containing the URL and other information 234 */ 235 public void setLogResponseSummary(boolean theValue) { 236 myLogResponseSummary = theValue; 237 } 238 239}