All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zipkin.reporter.AsyncReporter Maven / Gradle / Ivy

There is a newer version: 1.1.2
Show newest version
/**
 * 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;

import java.io.Flushable;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import zipkin.Component;
import zipkin.Span;

import static java.lang.String.format;
import static java.util.logging.Level.WARNING;
import static zipkin.internal.Util.checkArgument;
import static zipkin.internal.Util.checkNotNull;

/**
 * As spans are reported, they are encoded and added to a pending queue. The task of sending spans
 * happens on a separate thread which calls {@link #flush()}. By doing so, callers are protected
 * from latency or exceptions possible when exporting spans out of process.
 *
 * 

Spans are bundled into messages based on size in bytes or a timeout, whichever happens first. * * @param type of the span, usually {@link zipkin.Span} */ public abstract class AsyncReporter implements Reporter, Flushable, Component { /** * After a certain threshold, spans are drained and {@link Sender#sendSpans(List, Callback) sent} * to Zipkin collectors. */ public static Builder builder(Sender sender) { return new Builder(sender); } /** * Calling this will flush any pending spans to the transport on the current thread. * *

Note: If you set {@link Builder#messageTimeout(long, TimeUnit) message timeout} to zero, you * must call this externally as otherwise spans will never be sent. * * @throws IllegalStateException if closed */ @Override public abstract void flush(); /** Shuts down the sender thread, and increments drop metrics if there were any unsent spans. */ @Override public abstract void close(); public static final class Builder { final Sender sender; ReporterMetrics metrics = ReporterMetrics.NOOP_METRICS; int messageMaxBytes; long messageTimeoutNanos = TimeUnit.SECONDS.toNanos(1); int queuedMaxSpans = 10000; int queuedMaxBytes = onePercentOfMemory(); static int onePercentOfMemory() { long result = (long) (Runtime.getRuntime().totalMemory() * 0.01); // don't overflow in the rare case 1% of memory is larger than 2 GiB! return (int) Math.max(Math.min(Integer.MAX_VALUE, result), Integer.MIN_VALUE); } Builder(Sender sender) { this.sender = checkNotNull(sender, "sender"); this.messageMaxBytes = sender.messageMaxBytes(); } /** * Aggregates and reports reporter metrics to a monitoring system. Should be {@link * ReporterMetrics#forTransport(String) scoped to this transport}. Defaults to no-op. */ public Builder metrics(ReporterMetrics metrics) { this.metrics = checkNotNull(metrics, "metrics"); return this; } /** * Maximum bytes sendable per message including overhead. Defaults to, and is limited by {@link * Sender#messageMaxBytes()}. */ public Builder messageMaxBytes(int messageMaxBytes) { checkArgument(messageMaxBytes >= 0, "messageMaxBytes < 0: %s", messageMaxBytes); this.messageMaxBytes = Math.min(messageMaxBytes, sender.messageMaxBytes()); return this; } /** * Default 1 second. 0 implies spans are {@link #flush() flushed} externally. * *

Instead of sending one message at a time, spans are bundled into messages, up to {@link * Sender#messageMaxBytes()}. This timeout ensures that spans are not stuck in an incomplete * message. * *

Note: this timeout starts when the first unsent span is reported. */ public Builder messageTimeout(long timeout, TimeUnit unit) { checkArgument(timeout >= 0, "timeout < 0: %s", timeout); this.messageTimeoutNanos = unit.toNanos(checkNotNull(timeout, "timeout")); return this; } /** Maximum backlog of spans reported vs sent. Default 10000 */ public Builder queuedMaxSpans(int queuedMaxSpans) { this.queuedMaxSpans = queuedMaxSpans; return this; } /** Maximum backlog of span bytes reported vs sent. Default 1% of heap */ public Builder queuedMaxBytes(int queuedMaxBytes) { this.queuedMaxBytes = queuedMaxBytes; return this; } /** Builds an async reporter that encodes zipkin spans as they are reported. */ public AsyncReporter build() { switch (sender.encoding()) { case JSON: return build(Encoder.JSON); case THRIFT: return build(Encoder.THRIFT); default: throw new UnsupportedOperationException(sender.encoding().name()); } } /** Builds an async reporter that encodes arbitrary spans as they are reported. */ public AsyncReporter build(Encoder encoder) { checkNotNull(encoder, "encoder"); checkArgument(encoder.encoding() == sender.encoding(), "Encoder.encoding() %s != Sender.encoding() %s", encoder.encoding(), sender.encoding()); final BoundedAsyncReporter result = new BoundedAsyncReporter<>(this, encoder); if (messageTimeoutNanos > 0) { // Start a thread that flushes the queue in a loop. final BufferNextMessage consumer = new BufferNextMessage(sender, messageMaxBytes, messageTimeoutNanos); new Thread(() -> { try { while (!result.closed.get()) { result.flush(consumer); } } finally { for (byte[] next : consumer.drain()) result.pending.offer(next); result.close.countDown(); } }, "AsyncReporter(" + sender + ")").start(); } return result; } } static final class BoundedAsyncReporter extends AsyncReporter { static final Logger logger = Logger.getLogger(BoundedAsyncReporter.class.getName()); final AtomicBoolean closed = new AtomicBoolean(false); final Encoder encoder; final ByteBoundedQueue pending; final Sender sender; final int messageMaxBytes; final long messageTimeoutNanos; final CountDownLatch close; final ReporterMetrics metrics; BoundedAsyncReporter(Builder builder, Encoder encoder) { this.pending = new ByteBoundedQueue(builder.queuedMaxSpans, builder.queuedMaxBytes); this.sender = builder.sender; this.messageMaxBytes = builder.messageMaxBytes; this.messageTimeoutNanos = builder.messageTimeoutNanos; this.close = new CountDownLatch(builder.messageTimeoutNanos > 0 ? 1 : 0); this.metrics = builder.metrics; this.encoder = encoder; } /** Returns true if the was encoded and accepted onto the queue. */ @Override public void report(S span) { checkNotNull(span, "span"); metrics.incrementSpans(1); byte[] next = encoder.encode(span); int messageSizeOfNextSpan = sender.messageSizeInBytes(Collections.singletonList(next)); metrics.incrementSpanBytes(next.length); if (closed.get() || // don't enqueue something larger than we can drain messageSizeOfNextSpan > messageMaxBytes || !pending.offer(next)) { metrics.incrementSpansDropped(1); } } @Override public final void flush() { flush(new BufferNextMessage(sender, messageMaxBytes, 0)); } void flush(BufferNextMessage bundler) { if (closed.get()) throw new IllegalStateException("closed"); pending.drainTo(bundler, bundler.remainingNanos()); if (!bundler.isReady()) return; // try to fill up the bundle // Signal that we are about to send a message of a known size in bytes metrics.incrementMessages(); metrics.incrementMessageBytes(bundler.sizeInBytes()); List nextMessage = bundler.drain(); // In failure case, we increment messages and spans dropped. Callback failureCallback = sendSpansCallback(nextMessage.size()); try { sender.sendSpans(nextMessage, failureCallback); } catch (RuntimeException e) { failureCallback.onError(e); // Raise in case the sender was closed out-of-band. if (e instanceof IllegalStateException) throw e; } } @Override public CheckResult check() { return sender.check(); } @Override public void close() { closed.set(true); try { if (!close.await(messageTimeoutNanos, TimeUnit.NANOSECONDS)) { logger.warning("Timed out waiting for close"); } } catch (InterruptedException e) { logger.warning("Interrupted waiting for close"); Thread.currentThread().interrupt(); } int count = pending.clear(); if (count > 0) { metrics.incrementSpansDropped(count); logger.warning("Dropped " + count + " spans due to AsyncReporter.close()"); } } Callback sendSpansCallback(final int count) { return new Callback() { @Override public void onComplete() { } @Override public void onError(Throwable t) { metrics.incrementMessagesDropped(); metrics.incrementSpansDropped(count); logger.log(WARNING, format("Dropped %s spans due to %s(%s)", count, t.getClass().getSimpleName(), t.getMessage() == null ? "" : t.getMessage()), t); } }; } @Override public String toString() { return "AsyncReporter(" + sender + ")"; } } }