001package ca.uhn.fhir.rest.client.interceptor;
002
003/*
004 * #%L
005 * HAPI FHIR - Client Framework
006 * %%
007 * Copyright (C) 2014 - 2020 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 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}