zipkin.reporter.okhttp3.OkHttpSender Maven / Gradle / Ivy
/**
* Copyright 2016 The OpenZipkin Authors
*
* 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 zipkin.reporter.okhttp3;
import com.google.auto.value.AutoValue;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Dispatcher;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.Util;
import okio.Buffer;
import okio.BufferedSink;
import okio.GzipSink;
import okio.Okio;
import zipkin.internal.LazyCloseable;
import zipkin.reporter.Callback;
import zipkin.reporter.Encoding;
import zipkin.reporter.Sender;
import static zipkin.internal.Util.checkArgument;
import static zipkin.internal.Util.checkNotNull;
/**
* Reports spans to Zipkin, using its POST endpoint.
*
* This sender is thread-safe.
*/
@AutoValue
public abstract class OkHttpSender extends LazyCloseable implements Sender {
/** Creates a sender that posts {@link Encoding#THRIFT thrift} messages. */
public static OkHttpSender create(String endpoint) {
return builder().endpoint(endpoint).build();
}
public static Builder builder() {
return new AutoValue_OkHttpSender.Builder()
.encoding(Encoding.THRIFT)
.compressionEnabled(true)
.maxRequests(64)
.messageMaxBytes(5 * 1024 * 1024);
}
@AutoValue.Builder
public static abstract class Builder {
/**
* No default. The POST URL for zipkin's v1 api,
* usually "http://zipkinhost:9411/api/v1/spans"
*/
// customizable so that users can re-map /api/v1/spans ex for browser-originated traces
public final Builder endpoint(String endpoint) {
checkNotNull(endpoint, "endpoint ex: http://zipkinhost:9411/api/v1/spans");
HttpUrl parsed = HttpUrl.parse(endpoint);
checkArgument(parsed != null, "invalid post url: " + endpoint);
return endpoint(parsed);
}
public abstract Builder endpoint(HttpUrl endpoint);
/** Default true. true implies that spans will be gzipped before transport. */
public abstract Builder compressionEnabled(boolean compressSpans);
/** Maximum size of a message. Default 5MiB */
public abstract Builder messageMaxBytes(int messageMaxBytes);
/** Maximum in-flight requests. Default 64 */
public abstract Builder maxRequests(int maxRequests);
/** Controls the "Content-Type" header when sending spans. */
public abstract Builder encoding(Encoding encoding);
abstract int maxRequests();
abstract Encoding encoding();
public final OkHttpSender build() {
if (encoding() == Encoding.JSON) {
return encoder(RequestBodyMessageEncoder.JSON).autoBuild();
} else if (encoding() == Encoding.THRIFT) {
return encoder(RequestBodyMessageEncoder.THRIFT).autoBuild();
}
throw new UnsupportedOperationException("Unsupported encoding: " + encoding().name());
}
abstract Builder encoder(RequestBodyMessageEncoder encoder);
abstract OkHttpSender autoBuild();
Builder() {
}
}
public Builder toBuilder() {
return new AutoValue_OkHttpSender.Builder(this);
}
abstract HttpUrl endpoint();
abstract int maxRequests();
abstract boolean compressionEnabled();
abstract RequestBodyMessageEncoder encoder();
@Override public int messageSizeInBytes(List encodedSpans) {
return encoding().listSizeInBytes(encodedSpans);
}
/** close is typically called from a different thread */
transient boolean closeCalled;
/** Asynchronously sends the spans as a POST to {@link #endpoint()}. */
@Override public void sendSpans(List encodedSpans, Callback callback) {
if (closeCalled) throw new IllegalStateException("closed");
try {
Request request = newRequest(encoder().encode(encodedSpans));
get().newCall(request).enqueue(new CallbackAdapter(callback));
} catch (Throwable e) {
callback.onError(e);
if (e instanceof Error) throw (Error) e;
}
}
/** Sends an empty json message to the configured endpoint. */
@Override public CheckResult check() {
try {
Request request = new Request.Builder().url(endpoint())
.post(RequestBody.create(MediaType.parse("application/json"), "[]")).build();
try (Response response = get().newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IllegalStateException("check response failed: " + response);
}
}
return CheckResult.OK;
} catch (Exception e) {
return CheckResult.failed(e);
}
}
@Override protected OkHttpClient compute() {
// bound the executor so that we get consistent performance
ThreadPoolExecutor dispatchExecutor =
new ThreadPoolExecutor(0, maxRequests(), 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(maxRequests()),
Util.threadFactory("OkHttpSender Dispatcher", false));
Dispatcher dispatcher = new Dispatcher(dispatchExecutor);
dispatcher.setMaxRequests(maxRequests());
dispatcher.setMaxRequestsPerHost(maxRequests());
return new OkHttpClient.Builder().dispatcher(dispatcher).build();
}
/** Waits up to a second for in-flight requests to finish before cancelling them */
@Override public void close() {
if (closeCalled) return;
closeCalled = true;
OkHttpClient maybeNull = maybeNull();
if (maybeNull == null) return;
Dispatcher dispatcher = maybeNull.dispatcher();
dispatcher.executorService().shutdown();
try {
if (!dispatcher.executorService().awaitTermination(1, TimeUnit.SECONDS)) {
dispatcher.cancelAll();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Request newRequest(RequestBody body) throws IOException {
Request.Builder request = new Request.Builder().url(endpoint());
if (compressionEnabled()) {
request.addHeader("Content-Encoding", "gzip");
Buffer gzipped = new Buffer();
BufferedSink gzipSink = Okio.buffer(new GzipSink(gzipped));
body.writeTo(gzipSink);
gzipSink.close();
body = new BufferRequestBody(body.contentType(), gzipped);
}
request.post(body);
return request.build();
}
OkHttpSender() {
}
static final class BufferRequestBody extends RequestBody {
private final MediaType contentType;
private final Buffer body;
public BufferRequestBody(MediaType contentType, Buffer body) {
this.contentType = contentType;
this.body = body;
}
@Override public MediaType contentType() {
return contentType;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.write(body, body.size());
}
}
static final class CallbackAdapter implements okhttp3.Callback {
private final Callback delegate;
public CallbackAdapter(Callback delegate) {
this.delegate = delegate;
}
@Override public void onFailure(Call call, IOException e) {
delegate.onError(e);
}
@Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (response.isSuccessful()) {
delegate.onComplete();
} else {
delegate.onError(new IllegalStateException("response failed: " + response));
}
}
}
@Override public String toString() {
return "CallbackAdapter(" + delegate + ")";
}
}
}