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}