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}