diff --git a/okcurl/pom.xml b/okcurl/pom.xml index 6d1cc5c69703..c8a93b7307d7 100644 --- a/okcurl/pom.xml +++ b/okcurl/pom.xml @@ -18,6 +18,11 @@ okhttp ${project.version} + + ${project.groupId} + logging-interceptor + ${project.version} + ${project.groupId} okhttp-testing-support diff --git a/okcurl/src/main/java/okhttp3/curl/Main.java b/okcurl/src/main/java/okhttp3/curl/Main.java index cee260061974..4b1fd210871e 100644 --- a/okcurl/src/main/java/okhttp3/curl/Main.java +++ b/okcurl/src/main/java/okhttp3/curl/Main.java @@ -48,6 +48,8 @@ import okhttp3.internal.http.StatusLine; import okhttp3.internal.http2.Http2; import okhttp3.internal.platform.Platform; +import okhttp3.logging.HttpLoggingInterceptor; +import okhttp3.logging.LoggingEventListener; import okio.BufferedSource; import okio.Okio; import okio.Sink; @@ -121,6 +123,11 @@ private static String protocols() { @Option(name = {"-V", "--version"}, description = "Show version number and quit") public boolean version; + @Option( + name = {"-v", "--verbose"}, + description = "Makes " + NAME + " verbose during the operation") + public boolean verbose; + @Arguments(title = "url", description = "Remote resource URL") public String url; @@ -184,6 +191,16 @@ private OkHttpClient createClient() { builder.sslSocketFactory(sslSocketFactory, trustManager); builder.hostnameVerifier(createInsecureHostnameVerifier()); } + if (verbose) { + HttpLoggingInterceptor.Logger logger = + new HttpLoggingInterceptor.Logger() { + @Override + public void log(String message) { + System.out.println(message); + } + }; + builder.eventListenerFactory(new LoggingEventListener.Factory(logger)); + } return builder.build(); } diff --git a/okhttp-logging-interceptor/src/main/java/okhttp3/logging/LoggingEventListener.java b/okhttp-logging-interceptor/src/main/java/okhttp3/logging/LoggingEventListener.java new file mode 100644 index 000000000000..cc5adde48e9a --- /dev/null +++ b/okhttp-logging-interceptor/src/main/java/okhttp3/logging/LoggingEventListener.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.logging; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import okhttp3.Call; +import okhttp3.Connection; +import okhttp3.EventListener; +import okhttp3.Handshake; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; + +/** + * An OkHttp EventListener, which logs call events. Can be applied as an + * {@linkplain OkHttpClient#eventListenerFactory() event listener factory}. + * + *

The format of the logs created by this class should not be considered stable and may change + * slightly between releases. If you need a stable logging format, use your own event listener. + */ +public final class LoggingEventListener extends EventListener { + private final HttpLoggingInterceptor.Logger logger; + private long startNs; + + private LoggingEventListener(HttpLoggingInterceptor.Logger logger) { + this.logger = logger; + } + + @Override + public void callStart(Call call) { + startNs = System.nanoTime(); + + logWithTime("callStart: " + call.request()); + } + + @Override + public void dnsStart(Call call, String domainName) { + logWithTime("dnsStart: " + domainName); + } + + @Override + public void dnsEnd(Call call, String domainName, List inetAddressList) { + logWithTime("dnsEnd: " + inetAddressList); + } + + @Override + public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { + logWithTime("connectStart: " + inetSocketAddress + " " + proxy); + } + + @Override + public void secureConnectStart(Call call) { + logWithTime("secureConnectStart"); + } + + @Override + public void secureConnectEnd(Call call, @Nullable Handshake handshake) { + logWithTime("secureConnectEnd"); + } + + @Override + public void connectEnd( + Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) { + logWithTime("connectEnd: " + protocol); + } + + @Override + public void connectFailed( + Call call, + InetSocketAddress inetSocketAddress, + Proxy proxy, + @Nullable Protocol protocol, + IOException ioe) { + logWithTime("connectFailed: " + protocol + " " + ioe); + } + + @Override + public void connectionAcquired(Call call, Connection connection) { + logWithTime("connectionAcquired: " + connection); + } + + @Override + public void connectionReleased(Call call, Connection connection) { + logWithTime("connectionReleased"); + } + + @Override + public void requestHeadersStart(Call call) { + logWithTime("requestHeadersStart"); + } + + @Override + public void requestHeadersEnd(Call call, Request request) { + logWithTime("requestHeadersEnd"); + } + + @Override + public void requestBodyStart(Call call) { + logWithTime("requestBodyStart"); + } + + @Override + public void requestBodyEnd(Call call, long byteCount) { + logWithTime("requestBodyEnd: byteCount=" + byteCount); + } + + @Override + public void responseHeadersStart(Call call) { + logWithTime("responseHeadersStart"); + } + + @Override + public void responseHeadersEnd(Call call, Response response) { + logWithTime("responseHeadersEnd: " + response); + } + + @Override + public void responseBodyStart(Call call) { + logWithTime("responseBodyStart"); + } + + @Override + public void responseBodyEnd(Call call, long byteCount) { + logWithTime("responseBodyEnd: byteCount=" + byteCount); + } + + @Override + public void callEnd(Call call) { + logWithTime("callEnd"); + } + + @Override + public void callFailed(Call call, IOException ioe) { + logWithTime("callFailed: " + ioe); + } + + private void logWithTime(String message) { + long timeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + logger.log("[" + timeMs + " ms] " + message); + } + + public static class Factory implements EventListener.Factory { + private final HttpLoggingInterceptor.Logger logger; + + public Factory() { + this(HttpLoggingInterceptor.Logger.DEFAULT); + } + + public Factory(HttpLoggingInterceptor.Logger logger) { + this.logger = logger; + } + + @Override + public EventListener create(Call call) { + return new LoggingEventListener(logger); + } + } +} diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java index b2acd9051ac0..48e86ab9cda3 100644 --- a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java +++ b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java @@ -807,7 +807,7 @@ private Request.Builder request() { return new Request.Builder().url(url); } - private static class LogRecorder implements HttpLoggingInterceptor.Logger { + static class LogRecorder implements HttpLoggingInterceptor.Logger { private final List logs = new ArrayList<>(); private int index; diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java new file mode 100644 index 000000000000..0f0ea2bd715e --- /dev/null +++ b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.logging; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import okhttp3.Dns; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.SocketPolicy; +import okhttp3.tls.HandshakeCertificates; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static okhttp3.tls.internal.TlsUtil.localhost; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public final class LoggingEventListenerTest { + private static final MediaType PLAIN = MediaType.get("text/plain"); + + @Rule public final MockWebServer server = new MockWebServer(); + + private final HandshakeCertificates handshakeCertificates = localhost(); + private final LogRecorder logRecorder = new LogRecorder(); + private final LoggingEventListener.Factory loggingEventListenerFactory = + new LoggingEventListener.Factory(logRecorder); + private OkHttpClient client; + private HttpUrl url; + + @Before + public void setUp() { + client = + new OkHttpClient.Builder() + .eventListenerFactory(loggingEventListenerFactory) + .sslSocketFactory( + handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager()) + .retryOnConnectionFailure(false) + .build(); + + url = server.url("/"); + } + + @Test + public void get() throws Exception { + server.enqueue(new MockResponse().setBody("Hello!").setHeader("Content-Type", PLAIN)); + Response response = client.newCall(request().build()).execute(); + assertNotNull(response.body()); + response.body().bytes(); + + logRecorder + .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}") + .assertLogMatch("dnsStart: " + url.host()) + .assertLogMatch("dnsEnd: \\[.+\\]") + .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") + .assertLogMatch("connectEnd: http/1.1") + .assertLogMatch( + "connectionAcquired: Connection\\{" + + url.host() + + ":\\d+, proxy=DIRECT hostAddress=" + + url.host() + + "/.+ cipherSuite=none protocol=http/1\\.1\\}") + .assertLogMatch("requestHeadersStart") + .assertLogMatch("requestHeadersEnd") + .assertLogMatch("responseHeadersStart") + .assertLogMatch( + "responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url=" + + url + + "}") + .assertLogMatch("responseBodyStart") + .assertLogMatch("responseBodyEnd: byteCount=6") + .assertLogMatch("connectionReleased") + .assertLogMatch("callEnd") + .assertNoMoreLogs(); + } + + @Test + public void post() throws IOException { + server.enqueue(new MockResponse()); + client.newCall(request().post(RequestBody.create(PLAIN, "Hello!")).build()).execute(); + + logRecorder + .assertLogMatch("callStart: Request\\{method=POST, url=" + url + ", tags=\\{\\}\\}") + .assertLogMatch("dnsStart: " + url.host()) + .assertLogMatch("dnsEnd: \\[.+\\]") + .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") + .assertLogMatch("connectEnd: http/1.1") + .assertLogMatch( + "connectionAcquired: Connection\\{" + + url.host() + + ":\\d+, proxy=DIRECT hostAddress=" + + url.host() + + "/.+ cipherSuite=none protocol=http/1\\.1\\}") + .assertLogMatch("requestHeadersStart") + .assertLogMatch("requestHeadersEnd") + .assertLogMatch("requestBodyStart") + .assertLogMatch("requestBodyEnd: byteCount=6") + .assertLogMatch("responseHeadersStart") + .assertLogMatch( + "responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url=" + + url + + "}") + .assertLogMatch("responseBodyStart") + .assertLogMatch("responseBodyEnd: byteCount=0") + .assertLogMatch("connectionReleased") + .assertLogMatch("callEnd") + .assertNoMoreLogs(); + } + + @Test + public void secureGet() throws Exception { + server.useHttps(handshakeCertificates.sslSocketFactory(), false); + url = server.url("/"); + + server.enqueue(new MockResponse()); + Response response = client.newCall(request().build()).execute(); + assertNotNull(response.body()); + response.body().bytes(); + + logRecorder + .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}") + .assertLogMatch("dnsStart: " + url.host()) + .assertLogMatch("dnsEnd: \\[.+\\]") + .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") + .assertLogMatch("secureConnectStart") + .assertLogMatch("secureConnectEnd") + .assertLogMatch("connectEnd: h2") + .assertLogMatch( + "connectionAcquired: Connection\\{" + + url.host() + + ":\\d+, proxy=DIRECT hostAddress=" + + url.host() + + "/.+ cipherSuite=.+ protocol=h2}") + .assertLogMatch("requestHeadersStart") + .assertLogMatch("requestHeadersEnd") + .assertLogMatch("responseHeadersStart") + .assertLogMatch( + "responseHeadersEnd: Response\\{protocol=h2, code=200, message=, url=" + url + "}") + .assertLogMatch("responseBodyStart") + .assertLogMatch("responseBodyEnd: byteCount=0") + .assertLogMatch("connectionReleased") + .assertLogMatch("callEnd") + .assertNoMoreLogs(); + } + + @Test + public void dnsFail() throws IOException { + client = + new OkHttpClient.Builder() + .dns( + new Dns() { + @Override + public List lookup(String hostname) throws UnknownHostException { + throw new UnknownHostException("reason"); + } + }) + .eventListenerFactory(loggingEventListenerFactory) + .build(); + + try { + client.newCall(request().build()).execute(); + fail(); + } catch (UnknownHostException expected) { + } + + logRecorder + .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}") + .assertLogMatch("dnsStart: " + url.host()) + .assertLogMatch("callFailed: java.net.UnknownHostException: reason") + .assertNoMoreLogs(); + } + + @Test + public void connectFail() { + server.useHttps(handshakeCertificates.sslSocketFactory(), false); + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); + url = server.url("/"); + + try { + client.newCall(request().build()).execute(); + fail(); + } catch (IOException expected) { + } + + logRecorder + .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}") + .assertLogMatch("dnsStart: " + url.host()) + .assertLogMatch("dnsEnd: \\[.+\\]") + .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT") + .assertLogMatch("secureConnectStart") + .assertLogMatch( + "connectFailed: null javax\\.net\\.ssl\\.SSLProtocolException: Handshake message sequence violation, 1") + .assertLogMatch( + "callFailed: javax.net.ssl.SSLProtocolException: Handshake message sequence violation, 1") + .assertNoMoreLogs(); + } + + private Request.Builder request() { + return new Request.Builder().url(url); + } + + private static class LogRecorder extends HttpLoggingInterceptorTest.LogRecorder { + LogRecorder assertLogMatch(String pattern) { + return (LogRecorder) super.assertLogMatch("\\[\\d+ ms] " + pattern); + } + } +}