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}