Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New 'Invocation' class for tracking metrics on Retrofit calls #2899

Merged
merged 1 commit into from Sep 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion retrofit-mock/src/main/java/retrofit2/mock/Calls.java
Expand Up @@ -132,7 +132,9 @@ private static <T extends Throwable> T sneakyThrow2(Throwable t) throws T {
if (response != null) {
return response.raw().request();
}
return new Request.Builder().url("http://localhost").build();
return new Request.Builder()
.url("http://localhost")
.build();
}
}

Expand Down
2 changes: 0 additions & 2 deletions retrofit-mock/src/test/java/retrofit2/mock/CallsTest.java
Expand Up @@ -15,7 +15,6 @@
*/
package retrofit2.mock;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.util.concurrent.Callable;
Expand All @@ -24,7 +23,6 @@
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
Expand Down
76 changes: 76 additions & 0 deletions retrofit/src/main/java/retrofit2/Invocation.java
@@ -0,0 +1,76 @@
/*
* 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 retrofit2;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static retrofit2.Utils.checkNotNull;

/**
* A single invocation of a Retrofit service interface method. This class captures both the method
* that was called and the arguments to the method.
*
* <p>Retrofit automatically adds an invocation to each OkHttp request as a tag. You can retrieve
* the invocation in an OkHttp interceptor for metrics and monitoring.
*
* <pre><code>
* class InvocationLogger implements Interceptor {
* &#64;Override public Response intercept(Chain chain) throws IOException {
* Request request = chain.request();
* Invocation invocation = request.tag(Invocation.class);
* if (invocation != null) {
* System.out.printf("%s.%s %s%n",
* invocation.method().getDeclaringClass().getSimpleName(),
* invocation.method().getName(), invocation.arguments());
* }
* return chain.proceed(request);
* }
* }
* </code></pre>
*
* <strong>Note:</strong> use caution when examining an invocation's arguments. Although the
* arguments list is unmodifiable, the arguments themselves may be mutable. They may also be unsafe
* for concurrent access. For best results declare Retrofit service interfaces using only immutable
* types for parameters!
*/
public final class Invocation {
private final Method method;
private final List<?> arguments;

public Invocation(Method method, List<?> arguments) {
checkNotNull(method, "method == null");
checkNotNull(arguments, "arguments == null");

this.method = method;
this.arguments = Collections.unmodifiableList(new ArrayList<>(arguments)); // Immutable copy.
}

public Method method() {
return method;
}

public List<?> arguments() {
return arguments;
}

@Override public String toString() {
return String.format("%s.%s() %s",
method.getDeclaringClass().getName(), method.getName(), arguments);
}
}
9 changes: 5 additions & 4 deletions retrofit/src/main/java/retrofit2/RequestBuilder.java
Expand Up @@ -46,13 +46,14 @@ final class RequestBuilder {
private @Nullable FormBody.Builder formBuilder;
private @Nullable RequestBody body;

RequestBuilder(String method, HttpUrl baseUrl, @Nullable String relativeUrl,
@Nullable Headers headers, @Nullable MediaType contentType, boolean hasBody,
boolean isFormEncoded, boolean isMultipart) {
RequestBuilder(Invocation invocation, String method, HttpUrl baseUrl,
@Nullable String relativeUrl, @Nullable Headers headers, @Nullable MediaType contentType,
boolean hasBody, boolean isFormEncoded, boolean isMultipart) {
this.method = method;
this.baseUrl = baseUrl;
this.relativeUrl = relativeUrl;
this.requestBuilder = new Request.Builder();
this.requestBuilder = new Request.Builder()
.tag(Invocation.class, invocation);
this.contentType = contentType;
this.hasBody = hasBody;

Expand Down
10 changes: 7 additions & 3 deletions retrofit/src/main/java/retrofit2/RequestFactory.java
Expand Up @@ -21,6 +21,7 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -63,6 +64,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new Builder(retrofit, method).build();
}

private final Method method;
private final HttpUrl baseUrl;
final String httpMethod;
private final String relativeUrl;
Expand All @@ -74,6 +76,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
private final ParameterHandler<?>[] parameterHandlers;

RequestFactory(Builder builder) {
method = builder.method;
baseUrl = builder.retrofit.baseUrl;
httpMethod = builder.httpMethod;
relativeUrl = builder.relativeUrl;
Expand All @@ -86,9 +89,6 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
}

okhttp3.Request create(Object[] args) throws IOException {
RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
contentType, hasBody, isFormEncoded, isMultipart);

@SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;

Expand All @@ -98,6 +98,10 @@ okhttp3.Request create(Object[] args) throws IOException {
+ ") doesn't match expected count (" + handlers.length + ")");
}

Invocation invocation = new Invocation(method, Arrays.asList(args));
RequestBuilder requestBuilder = new RequestBuilder(invocation, httpMethod, baseUrl, relativeUrl,
headers, contentType, hasBody, isFormEncoded, isMultipart);

for (int p = 0; p < argumentCount; p++) {
handlers[p].apply(requestBuilder, args[p]);
}
Expand Down
88 changes: 88 additions & 0 deletions retrofit/src/test/java/retrofit2/InvocationTest.java
@@ -0,0 +1,88 @@
/*
* 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 retrofit2;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import org.junit.Test;
import retrofit2.http.Body;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

public final class InvocationTest {
interface Example {
@POST("/{p1}") //
Call<ResponseBody> postMethod(
@Path("p1") String p1, @Query("p2") String p2, @Body RequestBody body);
}

@Test public void invocationObjectOnCallAndRequestTag() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://example.com/")
.callFactory(new OkHttpClient())
.build();

Example example = retrofit.create(Example.class);
RequestBody requestBody = RequestBody.create(MediaType.get("text/plain"), "three");
Call<ResponseBody> call = example.postMethod("one", "two", requestBody);

Invocation invocation = call.request().tag(Invocation.class);
Method method = invocation.method();
assertThat(method.getName()).isEqualTo("postMethod");
assertThat(method.getDeclaringClass()).isEqualTo(Example.class);
assertThat(invocation.arguments()).isEqualTo(Arrays.asList("one", "two", requestBody));
}

@Test public void nullMethod() {
try {
new Invocation(null, Arrays.asList("one", "two"));
fail();
} catch (NullPointerException expected) {
assertThat(expected).hasMessage("method == null");
}
}

@Test public void nullArguments() {
try {
new Invocation(Example.class.getDeclaredMethods()[0], null);
fail();
} catch (NullPointerException expected) {
assertThat(expected).hasMessage("arguments == null");
}
}

@Test public void argumentsAreImmutable() {
List<String> mutableList = new ArrayList<>(Arrays.asList("one", "two"));
Invocation invocation = new Invocation(Example.class.getDeclaredMethods()[0], mutableList);
mutableList.add("three");
assertThat(invocation.arguments()).isEqualTo(Arrays.asList("one", "two"));
try {
invocation.arguments().clear();
fail();
} catch (UnsupportedOperationException expected) {
}
}
}
89 changes: 89 additions & 0 deletions samples/src/main/java/com/example/retrofit/InvocationMetrics.java
@@ -0,0 +1,89 @@
/*
* 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 com.example.retrofit;

import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Invocation;
import retrofit2.Retrofit;
import retrofit2.http.GET;
import retrofit2.http.Url;

/**
* This example prints HTTP call metrics with the initiating method names and arguments.
*/
public final class InvocationMetrics {
public interface Browse {
@GET("/robots.txt")
Call<ResponseBody> robots();

@GET("/favicon.ico")
Call<ResponseBody> favicon();

@GET("/")
Call<ResponseBody> home();

@GET
Call<ResponseBody> page(@Url String path);
}

static final class InvocationLogger implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startNanos = System.nanoTime();
Response response = chain.proceed(request);
long elapsedNanos = System.nanoTime() - startNanos;

Invocation invocation = request.tag(Invocation.class);
if (invocation != null) {
System.out.printf("%s.%s %s HTTP %s (%.0f ms)%n",
invocation.method().getDeclaringClass().getSimpleName(),
invocation.method().getName(),
invocation.arguments(),
response.code(),
elapsedNanos / 1_000_000.0);
}

return response;
}
}

public static void main(String... args) throws IOException {
InvocationLogger invocationLogger = new InvocationLogger();

OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(invocationLogger)
.build();

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://square.com/")
.callFactory(okHttpClient)
.build();

Browse browse = retrofit.create(Browse.class);

browse.robots().execute();
browse.favicon().execute();
browse.home().execute();
browse.page("sitemap.xml").execute();
browse.page("notfound").execute();
}
}