zipkin2.collector.Collector Maven / Gradle / Ivy
/*
* Copyright The OpenZipkin Authors
* SPDX-License-Identifier: Apache-2.0
*/
package zipkin2.collector;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import zipkin2.Callback;
import zipkin2.Span;
import zipkin2.SpanBytesDecoderDetector;
import zipkin2.codec.BytesDecoder;
import zipkin2.codec.SpanBytesDecoder;
import zipkin2.storage.StorageComponent;
import static zipkin2.Call.propagateIfFatal;
/**
* This component takes action on spans received from a transport. This includes deserializing,
* sampling and scheduling for storage.
*
* Callbacks passed do not propagate to the storage layer. They only return success or failures
* before storage is attempted. This ensures that calling threads are disconnected from storage
* threads.
*/
public class Collector { // not final for mock
static final Callback NOOP_CALLBACK = new Callback() {
@Override public void onSuccess(Void value) {
}
@Override public void onError(Throwable t) {
}
};
/** Needed to scope this to the correct logging category */
public static Builder newBuilder(Class> loggingClass) {
if (loggingClass == null) throw new NullPointerException("loggingClass == null");
return new Builder(LoggerFactory.getLogger(loggingClass.getName()));
}
public static final class Builder {
final Logger logger;
StorageComponent storage;
CollectorSampler sampler;
CollectorMetrics metrics;
Builder(Logger logger) {
this.logger = logger;
}
/** Sets {@link {@link CollectorComponent.Builder#storage(StorageComponent)}} */
public Builder storage(StorageComponent storage) {
if (storage == null) throw new NullPointerException("storage == null");
this.storage = storage;
return this;
}
/** Sets {@link {@link CollectorComponent.Builder#metrics(CollectorMetrics)}} */
public Builder metrics(CollectorMetrics metrics) {
if (metrics == null) throw new NullPointerException("metrics == null");
this.metrics = metrics;
return this;
}
/** Sets {@link {@link CollectorComponent.Builder#sampler(CollectorSampler)}} */
public Builder sampler(CollectorSampler sampler) {
if (sampler == null) throw new NullPointerException("sampler == null");
this.sampler = sampler;
return this;
}
public Collector build() {
return new Collector(this);
}
}
final Logger logger;
final CollectorMetrics metrics;
final CollectorSampler sampler;
final StorageComponent storage;
Collector(Builder builder) {
if (builder.logger == null) throw new NullPointerException("logger == null");
this.logger = builder.logger;
this.metrics = builder.metrics == null ? CollectorMetrics.NOOP_METRICS : builder.metrics;
if (builder.storage == null) throw new NullPointerException("storage == null");
this.storage = builder.storage;
this.sampler = builder.sampler == null ? CollectorSampler.ALWAYS_SAMPLE : builder.sampler;
}
public void accept(List spans, Callback callback) {
accept(spans, callback, Runnable::run);
}
/**
* Calls to get the storage component could be blocking. This ensures requests that block
* callers (such as http or gRPC) do not add additional load during such events.
*
* @param executor the executor used to enqueue the storage request.
*/
public void accept(List spans, Callback callback, Executor executor) {
if (spans.isEmpty()) {
callback.onSuccess(null);
return;
}
metrics.incrementSpans(spans.size());
List sampledSpans = sample(spans);
if (sampledSpans.isEmpty()) {
callback.onSuccess(null);
return;
}
// In order to ensure callers are not blocked, we swap callbacks when we get to the storage
// phase of this process. Here, we create a callback whose sole purpose is classifying later
// errors on this bundle of spans in the same log category. This allows people to only turn on
// debug logging in one place.
try {
executor.execute(new StoreSpans(sampledSpans));
callback.onSuccess(null);
} catch (Throwable unexpected) { // ensure if a future is supplied we always set value or error
callback.onError(unexpected);
throw unexpected;
}
}
/** Like {@link #acceptSpans(byte[], BytesDecoder, Callback)}, except using a byte buffer. */
public void acceptSpans(ByteBuffer encoded, SpanBytesDecoder decoder, Callback callback,
Executor executor) {
List spans;
try {
spans = decoder.decodeList(encoded);
} catch (RuntimeException | Error e) {
handleDecodeError(e, callback);
return;
}
accept(spans, callback, executor);
}
/**
* Before calling this, call {@link CollectorMetrics#incrementMessages()}, and {@link
* CollectorMetrics#incrementBytes(int)}. Do not call any other metrics callbacks as those are
* handled internal to this method.
*
* @param serialized not empty message
*/
public void acceptSpans(byte[] serialized, Callback callback) {
BytesDecoder decoder;
try {
decoder = SpanBytesDecoderDetector.decoderForListMessage(serialized);
} catch (RuntimeException | Error e) {
handleDecodeError(e, callback);
return;
}
acceptSpans(serialized, decoder, callback);
}
/**
* Before calling this, call {@link CollectorMetrics#incrementMessages()}, and {@link
* CollectorMetrics#incrementBytes(int)}. Do not call any other metrics callbacks as those are
* handled internal to this method.
*
* @param serializedSpans not empty message
*/
public void acceptSpans(
byte[] serializedSpans, BytesDecoder decoder, Callback callback) {
List spans;
try {
spans = decodeList(decoder, serializedSpans);
} catch (RuntimeException | Error e) {
handleDecodeError(e, callback);
return;
}
accept(spans, callback);
}
List decodeList(BytesDecoder decoder, byte[] serialized) {
List out = new ArrayList<>();
decoder.decodeList(serialized, out);
return out;
}
void store(List sampledSpans, Callback callback) {
storage.spanConsumer().accept(sampledSpans).enqueue(callback);
}
String idString(Span span) {
return span.traceId() + "/" + span.id();
}
List sample(List input) {
List sampled = new ArrayList<>(input.size());
for (int i = 0, length = input.size(); i < length; i++) {
Span s = input.get(i);
if (sampler.isSampled(s.traceId(), Boolean.TRUE.equals(s.debug()))) {
sampled.add(s);
}
}
int dropped = input.size() - sampled.size();
if (dropped > 0) metrics.incrementSpansDropped(dropped);
return sampled;
}
class StoreSpans implements Callback, Runnable {
final List spans;
StoreSpans(List spans) {
this.spans = spans;
}
@Override public void run() {
try {
store(spans, this);
} catch (RuntimeException | Error e) {
// While unexpected, invoking the storage command could raise an error synchronously. When
// that's the case, we wouldn't have invoked callback.onSuccess, so we need to handle the
// error here.
onError(e);
}
}
@Override public void onSuccess(Void value) {
}
@Override public void onError(Throwable t) {
handleStorageError(spans, t, NOOP_CALLBACK);
}
@Override public String toString() {
return appendSpanIds(spans, new StringBuilder("StoreSpans(")) + ")";
}
}
void handleDecodeError(Throwable e, Callback callback) {
metrics.incrementMessagesDropped();
handleError(e, "Cannot decode spans"::toString, callback);
}
/**
* When storing spans, an exception can be raised before or after the fact. This adds context of
* span ids to give logs more relevance.
*/
void handleStorageError(List spans, Throwable e, Callback callback) {
metrics.incrementSpansDropped(spans.size());
// The exception could be related to a span being huge. Instead of filling logs,
// print trace id, span id pairs
handleError(e, () -> appendSpanIds(spans, new StringBuilder("Cannot store spans ")), callback);
}
void handleError(Throwable e, Supplier defaultLogMessage, Callback callback) {
propagateIfFatal(e);
callback.onError(e);
if (!logger.isDebugEnabled()) return;
String error = e.getMessage() != null ? e.getMessage() : "";
// We have specific code that customizes log messages. Use this when the case.
if (error.startsWith("Malformed") || error.startsWith("Truncated")) {
logger.debug(error, e);
} else { // otherwise, beautify the message
String message =
"%s due to %s(%s)".formatted(defaultLogMessage.get(), e.getClass().getSimpleName(), error);
logger.debug(message, e);
}
}
// TODO: this logic needs to be redone as service names are more important than span IDs. Also,
// span IDs repeat between client and server!
String appendSpanIds(List spans, StringBuilder message) {
message.append("[");
int i = 0;
Iterator iterator = spans.iterator();
while (iterator.hasNext() && i++ < 3) {
message.append(idString(iterator.next()));
if (iterator.hasNext()) message.append(", ");
}
if (iterator.hasNext()) message.append("...");
return message.append("]").toString();
}
}