1 /* 2 * Copyright (C) 2015 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.okhttp.logging; 17 18 import com.squareup.okhttp.Connection; 19 import com.squareup.okhttp.Headers; 20 import com.squareup.okhttp.Interceptor; 21 import com.squareup.okhttp.MediaType; 22 import com.squareup.okhttp.OkHttpClient; 23 import com.squareup.okhttp.Protocol; 24 import com.squareup.okhttp.Request; 25 import com.squareup.okhttp.RequestBody; 26 import com.squareup.okhttp.Response; 27 import com.squareup.okhttp.ResponseBody; 28 import com.squareup.okhttp.internal.Platform; 29 import com.squareup.okhttp.internal.http.HttpEngine; 30 import java.io.IOException; 31 import java.nio.charset.Charset; 32 import java.util.concurrent.TimeUnit; 33 import okio.Buffer; 34 import okio.BufferedSource; 35 36 /** 37 * An OkHttp interceptor which logs request and response information. Can be applied as an 38 * {@linkplain OkHttpClient#interceptors() application interceptor} or as a 39 * {@linkplain OkHttpClient#networkInterceptors() network interceptor}. 40 * <p> 41 * The format of the logs created by this class should not be considered stable and may change 42 * slightly between releases. If you need a stable logging format, use your own interceptor. 43 */ 44 public final class HttpLoggingInterceptor implements Interceptor { 45 private static final Charset UTF8 = Charset.forName("UTF-8"); 46 47 public enum Level { 48 /** No logs. */ 49 NONE, 50 /** 51 * Logs request and response lines. 52 * <p> 53 * Example: 54 * <pre>{@code 55 * --> POST /greeting HTTP/1.1 (3-byte body) 56 * 57 * <-- HTTP/1.1 200 OK (22ms, 6-byte body) 58 * }</pre> 59 */ 60 BASIC, 61 /** 62 * Logs request and response lines and their respective headers. 63 * <p> 64 * Example: 65 * <pre>{@code 66 * --> POST /greeting HTTP/1.1 67 * Host: example.com 68 * Content-Type: plain/text 69 * Content-Length: 3 70 * --> END POST 71 * 72 * <-- HTTP/1.1 200 OK (22ms) 73 * Content-Type: plain/text 74 * Content-Length: 6 75 * <-- END HTTP 76 * }</pre> 77 */ 78 HEADERS, 79 /** 80 * Logs request and response lines and their respective headers and bodies (if present). 81 * <p> 82 * Example: 83 * <pre>{@code 84 * --> POST /greeting HTTP/1.1 85 * Host: example.com 86 * Content-Type: plain/text 87 * Content-Length: 3 88 * 89 * Hi? 90 * --> END GET 91 * 92 * <-- HTTP/1.1 200 OK (22ms) 93 * Content-Type: plain/text 94 * Content-Length: 6 95 * 96 * Hello! 97 * <-- END HTTP 98 * }</pre> 99 */ 100 BODY 101 } 102 103 public interface Logger { 104 void log(String message); 105 106 /** A {@link Logger} defaults output appropriate for the current platform. */ 107 Logger DEFAULT = new Logger() { 108 @Override public void log(String message) { 109 Platform.get().log(message); 110 } 111 }; 112 } 113 114 public HttpLoggingInterceptor() { 115 this(Logger.DEFAULT); 116 } 117 118 public HttpLoggingInterceptor(Logger logger) { 119 this.logger = logger; 120 } 121 122 private final Logger logger; 123 124 private volatile Level level = Level.NONE; 125 126 /** Change the level at which this interceptor logs. */ 127 public HttpLoggingInterceptor setLevel(Level level) { 128 if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead."); 129 this.level = level; 130 return this; 131 } 132 133 public Level getLevel() { 134 return level; 135 } 136 137 @Override public Response intercept(Chain chain) throws IOException { 138 Level level = this.level; 139 140 Request request = chain.request(); 141 if (level == Level.NONE) { 142 return chain.proceed(request); 143 } 144 145 boolean logBody = level == Level.BODY; 146 boolean logHeaders = logBody || level == Level.HEADERS; 147 148 RequestBody requestBody = request.body(); 149 boolean hasRequestBody = requestBody != null; 150 151 Connection connection = chain.connection(); 152 Protocol protocol = connection != null ? connection.getProtocol() : Protocol.HTTP_1_1; 153 String requestStartMessage = 154 "--> " + request.method() + ' ' + request.httpUrl() + ' ' + protocol(protocol); 155 if (!logHeaders && hasRequestBody) { 156 requestStartMessage += " (" + requestBody.contentLength() + "-byte body)"; 157 } 158 logger.log(requestStartMessage); 159 160 if (logHeaders) { 161 if (hasRequestBody) { 162 // Request body headers are only present when installed as a network interceptor. Force 163 // them to be included (when available) so there values are known. 164 if (requestBody.contentType() != null) { 165 logger.log("Content-Type: " + requestBody.contentType()); 166 } 167 if (requestBody.contentLength() != -1) { 168 logger.log("Content-Length: " + requestBody.contentLength()); 169 } 170 } 171 172 Headers headers = request.headers(); 173 for (int i = 0, count = headers.size(); i < count; i++) { 174 String name = headers.name(i); 175 // Skip headers from the request body as they are explicitly logged above. 176 if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) { 177 logger.log(name + ": " + headers.value(i)); 178 } 179 } 180 181 if (!logBody || !hasRequestBody) { 182 logger.log("--> END " + request.method()); 183 } else if (bodyEncoded(request.headers())) { 184 logger.log("--> END " + request.method() + " (encoded body omitted)"); 185 } else { 186 Buffer buffer = new Buffer(); 187 requestBody.writeTo(buffer); 188 189 Charset charset = UTF8; 190 MediaType contentType = requestBody.contentType(); 191 if (contentType != null) { 192 contentType.charset(UTF8); 193 } 194 195 logger.log(""); 196 logger.log(buffer.readString(charset)); 197 198 logger.log("--> END " + request.method() 199 + " (" + requestBody.contentLength() + "-byte body)"); 200 } 201 } 202 203 long startNs = System.nanoTime(); 204 Response response = chain.proceed(request); 205 long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); 206 207 ResponseBody responseBody = response.body(); 208 logger.log("<-- " + protocol(response.protocol()) + ' ' + response.code() + ' ' 209 + response.message() + " (" + tookMs + "ms" 210 + (!logHeaders ? ", " + responseBody.contentLength() + "-byte body" : "") + ')'); 211 212 if (logHeaders) { 213 Headers headers = response.headers(); 214 for (int i = 0, count = headers.size(); i < count; i++) { 215 logger.log(headers.name(i) + ": " + headers.value(i)); 216 } 217 218 if (!logBody || !HttpEngine.hasBody(response)) { 219 logger.log("<-- END HTTP"); 220 } else if (bodyEncoded(response.headers())) { 221 logger.log("<-- END HTTP (encoded body omitted)"); 222 } else { 223 BufferedSource source = responseBody.source(); 224 source.request(Long.MAX_VALUE); // Buffer the entire body. 225 Buffer buffer = source.buffer(); 226 227 Charset charset = UTF8; 228 MediaType contentType = responseBody.contentType(); 229 if (contentType != null) { 230 charset = contentType.charset(UTF8); 231 } 232 233 if (responseBody.contentLength() != 0) { 234 logger.log(""); 235 logger.log(buffer.clone().readString(charset)); 236 } 237 238 logger.log("<-- END HTTP (" + buffer.size() + "-byte body)"); 239 } 240 } 241 242 return response; 243 } 244 245 private boolean bodyEncoded(Headers headers) { 246 String contentEncoding = headers.get("Content-Encoding"); 247 return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity"); 248 } 249 250 private static String protocol(Protocol protocol) { 251 return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1"; 252 } 253 } 254