Home | History | Annotate | Download | only in logging
      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