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 */
16package com.squareup.okhttp.logging;
17
18import com.squareup.okhttp.Connection;
19import com.squareup.okhttp.Headers;
20import com.squareup.okhttp.Interceptor;
21import com.squareup.okhttp.MediaType;
22import com.squareup.okhttp.OkHttpClient;
23import com.squareup.okhttp.Protocol;
24import com.squareup.okhttp.Request;
25import com.squareup.okhttp.RequestBody;
26import com.squareup.okhttp.Response;
27import com.squareup.okhttp.ResponseBody;
28import com.squareup.okhttp.internal.Platform;
29import com.squareup.okhttp.internal.http.HttpEngine;
30import java.io.IOException;
31import java.nio.charset.Charset;
32import java.util.concurrent.TimeUnit;
33import okio.Buffer;
34import 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 */
44public 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