
io.opentelemetry.sdk.trace.export.BatchSpanProcessor Maven / Gradle / Ivy
Show all versions of opentelemetry-sdk-trace Show documentation
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.trace.export;
import io.opentelemetry.api.common.Labels;
import io.opentelemetry.api.metrics.GlobalMetricsProvider;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.LongCounter.BoundLongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.internal.DaemonThreadFactory;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.data.SpanData;
import java.util.ArrayList;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Implementation of the {@link SpanProcessor} that batches spans exported by the SDK then pushes
* them to the exporter pipeline.
*
* All spans reported by the SDK implementation are first added to a synchronized queue (with a
* {@code maxQueueSize} maximum size, if queue is full spans are dropped). Spans are exported either
* when there are {@code maxExportBatchSize} pending spans or {@code scheduleDelayNanos} has passed
* since the last export finished.
*
*
This batch {@link SpanProcessor} can cause high contention in a very high traffic service.
* TODO: Add a link to the SpanProcessor that uses Disruptor as alternative with low contention.
*
*
Configuration options for {@link BatchSpanProcessor} can be read from system properties,
* environment variables, or {@link java.util.Properties} objects.
*
*
For system properties and {@link java.util.Properties} objects, {@link BatchSpanProcessor}
* will look for the following names:
*
*
* - {@code otel.bsp.schedule.delay}: sets the delay interval between two consecutive exports.
*
- {@code otel.bsp.max.queue}: sets the maximum queue size.
*
- {@code otel.bsp.max.export.batch}: sets the maximum batch size.
*
- {@code otel.bsp.export.timeout}: sets the maximum allowed time to export data.
*
- {@code otel.bsp.export.sampled}: sets whether only sampled spans should be exported.
*
*
* For environment variables, {@link BatchSpanProcessor} will look for the following names:
*
*
* - {@code OTEL_BSP_SCHEDULE_DELAY}: sets the delay interval between two consecutive exports.
*
- {@code OTEL_BSP_MAX_QUEUE}: sets the maximum queue size.
*
- {@code OTEL_BSP_MAX_EXPORT_BATCH}: sets the maximum batch size.
*
- {@code OTEL_BSP_EXPORT_TIMEOUT}: sets the maximum allowed time to export data.
*
- {@code OTEL_BSP_EXPORT_SAMPLED}: sets whether only sampled spans should be exported.
*
*/
public final class BatchSpanProcessor implements SpanProcessor {
private static final String WORKER_THREAD_NAME =
BatchSpanProcessor.class.getSimpleName() + "_WorkerThread";
private static final String SPAN_PROCESSOR_TYPE_LABEL = "spanProcessorType";
private static final String SPAN_PROCESSOR_TYPE_VALUE = BatchSpanProcessor.class.getSimpleName();
private final Worker worker;
private final boolean sampled;
private final AtomicBoolean isShutdown = new AtomicBoolean(false);
/**
* Returns a new Builder for {@link BatchSpanProcessor}.
*
* @param spanExporter the {@code SpanExporter} to where the Spans are pushed.
* @return a new {@link BatchSpanProcessor}.
* @throws NullPointerException if the {@code spanExporter} is {@code null}.
*/
public static BatchSpanProcessorBuilder builder(SpanExporter spanExporter) {
return new BatchSpanProcessorBuilder(spanExporter);
}
BatchSpanProcessor(
SpanExporter spanExporter,
boolean sampled,
long scheduleDelayNanos,
int maxQueueSize,
int maxExportBatchSize,
long exporterTimeoutNanos) {
this.worker =
new Worker(
spanExporter,
scheduleDelayNanos,
maxExportBatchSize,
exporterTimeoutNanos,
new ArrayBlockingQueue<>(maxQueueSize));
Thread workerThread = new DaemonThreadFactory(WORKER_THREAD_NAME).newThread(worker);
workerThread.start();
this.sampled = sampled;
}
@Override
public void onStart(Context parentContext, ReadWriteSpan span) {}
@Override
public boolean isStartRequired() {
return false;
}
@Override
public void onEnd(ReadableSpan span) {
if (sampled && !span.getSpanContext().isSampled()) {
return;
}
worker.addSpan(span);
}
@Override
public boolean isEndRequired() {
return true;
}
@Override
public CompletableResultCode shutdown() {
if (isShutdown.getAndSet(true)) {
return CompletableResultCode.ofSuccess();
}
return worker.shutdown();
}
@Override
public CompletableResultCode forceFlush() {
return worker.forceFlush();
}
// Worker is a thread that batches multiple spans and calls the registered SpanExporter to export
// the data.
private static final class Worker implements Runnable {
private final BoundLongCounter droppedSpans;
private final BoundLongCounter exportedSpans;
private static final Logger logger = Logger.getLogger(Worker.class.getName());
private final SpanExporter spanExporter;
private final long scheduleDelayNanos;
private final int maxExportBatchSize;
private final long exporterTimeoutNanos;
private long nextExportTime;
private final BlockingQueue queue;
private final AtomicReference flushRequested = new AtomicReference<>();
private volatile boolean continueWork = true;
private final ArrayList batch;
private Worker(
SpanExporter spanExporter,
long scheduleDelayNanos,
int maxExportBatchSize,
long exporterTimeoutNanos,
BlockingQueue queue) {
this.spanExporter = spanExporter;
this.scheduleDelayNanos = scheduleDelayNanos;
this.maxExportBatchSize = maxExportBatchSize;
this.exporterTimeoutNanos = exporterTimeoutNanos;
this.queue = queue;
Meter meter = GlobalMetricsProvider.getMeter("io.opentelemetry.sdk.trace");
meter
.longValueObserverBuilder("queueSize")
.setDescription("The number of spans queued")
.setUnit("1")
.setUpdater(
result ->
result.observe(
queue.size(),
Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE)))
.build();
LongCounter processedSpansCounter =
meter
.longCounterBuilder("processedSpans")
.setUnit("1")
.setDescription(
"The number of spans processed by the BatchSpanProcessor. "
+ "[dropped=true if they were dropped due to high throughput]")
.build();
droppedSpans =
processedSpansCounter.bind(
Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE, "dropped", "true"));
exportedSpans =
processedSpansCounter.bind(
Labels.of(SPAN_PROCESSOR_TYPE_LABEL, SPAN_PROCESSOR_TYPE_VALUE, "dropped", "false"));
this.batch = new ArrayList<>(this.maxExportBatchSize);
}
private void addSpan(ReadableSpan span) {
if (!queue.offer(span)) {
droppedSpans.add(1);
}
}
@Override
public void run() {
updateNextExportTime();
while (continueWork) {
if (flushRequested.get() != null) {
flush();
}
try {
ReadableSpan lastElement = queue.poll(100, TimeUnit.MILLISECONDS);
if (lastElement != null) {
batch.add(lastElement.toSpanData());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
if (batch.size() >= maxExportBatchSize || System.nanoTime() >= nextExportTime) {
exportCurrentBatch();
updateNextExportTime();
}
}
}
private void flush() {
int spansToFlush = queue.size();
while (spansToFlush > 0) {
ReadableSpan span = queue.poll();
assert span != null;
batch.add(span.toSpanData());
spansToFlush--;
if (batch.size() >= maxExportBatchSize) {
exportCurrentBatch();
}
}
exportCurrentBatch();
flushRequested.get().succeed();
flushRequested.set(null);
}
private void updateNextExportTime() {
nextExportTime = System.nanoTime() + scheduleDelayNanos;
}
private CompletableResultCode shutdown() {
final CompletableResultCode result = new CompletableResultCode();
final CompletableResultCode flushResult = forceFlush();
flushResult.whenComplete(
() -> {
continueWork = false;
final CompletableResultCode shutdownResult = spanExporter.shutdown();
shutdownResult.whenComplete(
() -> {
if (!flushResult.isSuccess() || !shutdownResult.isSuccess()) {
result.fail();
} else {
result.succeed();
}
});
});
return result;
}
private CompletableResultCode forceFlush() {
CompletableResultCode flushResult = new CompletableResultCode();
this.flushRequested.compareAndSet(null, flushResult);
return this.flushRequested.get();
}
private void exportCurrentBatch() {
if (batch.isEmpty()) {
return;
}
try {
final CompletableResultCode result = spanExporter.export(batch);
result.join(exporterTimeoutNanos, TimeUnit.NANOSECONDS);
if (result.isSuccess()) {
exportedSpans.add(batch.size());
} else {
logger.log(Level.FINE, "Exporter failed");
}
} catch (Exception e) {
logger.log(Level.WARNING, "Exporter threw an Exception", e);
} finally {
batch.clear();
}
}
}
}