diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt index 7c3f307df8e..07952e04955 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt @@ -1,2 +1,7 @@ Comparing source compatibility of opentelemetry-exporter-zipkin-1.59.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.58.0.jar -No changes. \ No newline at end of file +=== UNCHANGED CLASS: PUBLIC FINAL io.opentelemetry.exporter.zipkin.ZipkinSpanExporter (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW ANNOTATION: java.lang.Deprecated +=== UNCHANGED CLASS: PUBLIC FINAL io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW ANNOTATION: java.lang.Deprecated diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java index afd7bce4286..d71a9a234ce 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporter.java @@ -5,14 +5,14 @@ package io.opentelemetry.exporter.zipkin; -import io.opentelemetry.api.internal.InstrumentationUtil; import io.opentelemetry.api.metrics.MeterProvider; -import io.opentelemetry.exporter.internal.metrics.ExporterInstrumentation; +import io.opentelemetry.exporter.zipkin.internal.copied.ComponentId; +import io.opentelemetry.exporter.zipkin.internal.copied.ExporterInstrumentation; +import io.opentelemetry.exporter.zipkin.internal.copied.InstrumentationUtil; +import io.opentelemetry.exporter.zipkin.internal.copied.StandardComponentId; +import io.opentelemetry.exporter.zipkin.internal.copied.ThrottlingLogger; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InternalTelemetryVersion; -import io.opentelemetry.sdk.internal.ComponentId; -import io.opentelemetry.sdk.internal.StandardComponentId; -import io.opentelemetry.sdk.internal.ThrottlingLogger; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.IOException; @@ -32,7 +32,12 @@ * This class was based on the OpenCensus * zipkin exporter code. + * + * @deprecated Zipkin exporter is deprecated in OpenTelemetry spec (see the PR). + * Expect this artifact to no longer be published in approximately 6 months (mid 2026). */ +@Deprecated public final class ZipkinSpanExporter implements SpanExporter { public static final Logger baseLogger = Logger.getLogger(ZipkinSpanExporter.class.getName()); diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java index 8244905de8d..cae97769d9e 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java @@ -5,7 +5,6 @@ package io.opentelemetry.exporter.zipkin; -import static io.opentelemetry.api.internal.Utils.checkArgument; import static java.util.Objects.requireNonNull; import io.opentelemetry.api.GlobalOpenTelemetry; @@ -23,7 +22,14 @@ import zipkin2.reporter.SpanBytesEncoder; import zipkin2.reporter.okhttp3.OkHttpSender; -/** Builder class for {@link ZipkinSpanExporter}. */ +/** + * Builder class for {@link ZipkinSpanExporter}. + * + * @deprecated Zipkin exporter is deprecated in OpenTelemetry spec (see the PR). + * Expect this artifact to no longer be published in approximately 6 months (mid 2026). + */ +@Deprecated public final class ZipkinSpanExporterBuilder { private BytesEncoder encoder = SpanBytesEncoder.JSON_V2; private Supplier localIpAddressSupplier = LocalInetAddressSupplier.getInstance(); @@ -243,4 +249,12 @@ public ZipkinSpanExporter build() { endpoint, transformer); } + + // Copied from io.opentelemetry.api.internal.Utils to remove internal dependencies as part of + // https://github.com/open-telemetry/opentelemetry-java/issues/7863 + private static void checkArgument(boolean isValid, String errorMessage) { + if (!isValid) { + throw new IllegalArgumentException(errorMessage); + } + } } diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java index cbd55d386dc..f73446e6e40 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProvider.java @@ -5,19 +5,19 @@ package io.opentelemetry.exporter.zipkin.internal; -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.time.Duration; /** - * {@link SpanExporter} SPI implementation for {@link ZipkinSpanExporter}. + * {@link SpanExporter} SPI implementation for {@link + * io.opentelemetry.exporter.zipkin.ZipkinSpanExporter}. * *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ +@SuppressWarnings("deprecation") public class ZipkinSpanExporterProvider implements ConfigurableSpanExporterProvider { @Override public String getName() { @@ -26,7 +26,8 @@ public String getName() { @Override public SpanExporter createExporter(ConfigProperties config) { - ZipkinSpanExporterBuilder builder = ZipkinSpanExporter.builder(); + io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder builder = + io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.builder(); String endpoint = config.getString("otel.exporter.zipkin.endpoint"); if (endpoint != null) { diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ComponentId.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ComponentId.java new file mode 100644 index 00000000000..bbd6280b459 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ComponentId.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + +/** + * The component id used for SDK health metrics. This corresponds to the otel.component.name and + * otel.component.id semconv attributes. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public abstract class ComponentId { + + private ComponentId() {} + + public abstract String getTypeName(); + + public abstract String getComponentName(); + + static class Lazy extends ComponentId { + + private static final Map nextIdCounters = new ConcurrentHashMap<>(); + + private final String componentType; + @Nullable private volatile String componentName = null; + + Lazy(String componentType) { + this.componentType = componentType; + } + + @Override + public String getTypeName() { + return componentType; + } + + @Override + public String getComponentName() { + if (componentName == null) { + synchronized (this) { + if (componentName == null) { + int id = + nextIdCounters + .computeIfAbsent(componentType, k -> new AtomicInteger(0)) + .getAndIncrement(); + componentName = componentType + "/" + id; + } + } + } + return componentName; + } + } + + public static ComponentId generateLazy(String componentType) { + return new Lazy(componentType); + } + + public static StandardComponentId generateLazy(StandardComponentId.ExporterType exporterType) { + return new StandardComponentId(exporterType); + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterInstrumentation.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterInstrumentation.java new file mode 100644 index 00000000000..1f123978264 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterInstrumentation.java @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.InternalTelemetryVersion; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class ExporterInstrumentation { + + private final ExporterMetrics implementation; + + public ExporterInstrumentation( + InternalTelemetryVersion schema, + Supplier meterProviderSupplier, + StandardComponentId componentId, + String endpoint) { + + Signal signal = componentId.getStandardType().signal(); + switch (schema) { + case LEGACY: + implementation = + LegacyExporterMetrics.isSupportedType(componentId.getStandardType()) + ? new LegacyExporterMetrics(meterProviderSupplier, componentId.getStandardType()) + : NoopExporterMetrics.INSTANCE; + break; + case LATEST: + implementation = + signal == Signal.PROFILE + ? NoopExporterMetrics.INSTANCE + : new SemConvExporterMetrics( + meterProviderSupplier, signal, componentId, extractServerAttributes(endpoint)); + break; + default: + throw new IllegalStateException("Unhandled case: " + schema); + } + } + + // visible for testing + static Attributes extractServerAttributes(String httpEndpoint) { + try { + URI parsed = new URI(httpEndpoint); + AttributesBuilder builder = Attributes.builder(); + String host = parsed.getHost(); + if (host != null) { + builder.put(SemConvAttributes.SERVER_ADDRESS, host); + } + int port = parsed.getPort(); + if (port == -1) { + String scheme = parsed.getScheme(); + if ("https".equals(scheme)) { + port = 443; + } else if ("http".equals(scheme)) { + port = 80; + } + } + if (port != -1) { + builder.put(SemConvAttributes.SERVER_PORT, port); + } + return builder.build(); + } catch (URISyntaxException e) { + return Attributes.empty(); + } + } + + public Recording startRecordingExport(int itemCount) { + return new Recording(implementation.startRecordingExport(itemCount)); + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public static class Recording { + + private final ExporterMetrics.Recording delegate; + @Nullable private Long httpStatusCode; + @Nullable private Long grpcStatusCode; + + private Recording(ExporterMetrics.Recording delegate) { + this.delegate = delegate; + } + + public void setHttpStatusCode(long httpStatusCode) { + if (grpcStatusCode != null) { + throw new IllegalStateException( + "gRPC status code already set, can only set either gRPC or HTTP"); + } + this.httpStatusCode = httpStatusCode; + } + + public void setGrpcStatusCode(long grpcStatusCode) { + if (httpStatusCode != null) { + throw new IllegalStateException( + "HTTP status code already set, can only set either gRPC or HTTP"); + } + this.grpcStatusCode = grpcStatusCode; + } + + /** Callback to notify that the export was successful. */ + public void finishSuccessful() { + delegate.finishSuccessful(buildRequestAttributes()); + } + + /** + * Callback to notify that the export has failed with the given {@link Throwable} as failure + * cause. + * + * @param failureCause the cause of the failure + */ + public void finishFailed(Throwable failureCause) { + finishFailed(failureCause.getClass().getName()); + } + + /** + * Callback to notify that the export has failed. + * + * @param errorType a failure reason suitable for the error.type attribute + */ + public void finishFailed(String errorType) { + delegate.finishFailed(errorType, buildRequestAttributes()); + } + + private Attributes buildRequestAttributes() { + if (httpStatusCode != null) { + return Attributes.of(SemConvAttributes.HTTP_RESPONSE_STATUS_CODE, httpStatusCode); + } + if (grpcStatusCode != null) { + return Attributes.of(SemConvAttributes.RPC_GRPC_STATUS_CODE, grpcStatusCode); + } + return Attributes.empty(); + } + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterMetrics.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterMetrics.java new file mode 100644 index 00000000000..954f218a981 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterMetrics.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface ExporterMetrics { + + Recording startRecordingExport(int itemCount); + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + abstract class Recording { + + private boolean alreadyEnded = false; + + protected Recording() {} + + public final void finishSuccessful(Attributes requestAttributes) { + ensureEndedOnce(); + doFinish(null, requestAttributes); + } + + public final void finishFailed(String errorType, Attributes requestAttributes) { + ensureEndedOnce(); + if (errorType == null || errorType.isEmpty()) { + throw new IllegalArgumentException("The export failed but no failure reason was provided"); + } + doFinish(errorType, requestAttributes); + } + + private void ensureEndedOnce() { + if (alreadyEnded) { + throw new IllegalStateException("Recording already ended"); + } + alreadyEnded = true; + } + + /** + * Invoked when the export has finished, either successfully or failed. + * + * @param errorType null if the export was successful, otherwise a failure reason suitable for + * the error.type attribute + * @param requestAttributes additional attributes to add to request metrics + */ + protected abstract void doFinish(@Nullable String errorType, Attributes requestAttributes); + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/InstrumentationUtil.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/InstrumentationUtil.java new file mode 100644 index 00000000000..4eaa8117fb6 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/InstrumentationUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import java.util.Objects; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InstrumentationUtil { + private static final ContextKey SUPPRESS_INSTRUMENTATION_KEY = + ContextKey.named("suppress_instrumentation"); + + private InstrumentationUtil() {} + + /** + * Adds a Context boolean key that will allow to identify HTTP calls coming from OTel exporters. + * The key later be checked by an automatic instrumentation to avoid tracing OTel exporter's + * calls. + */ + public static void suppressInstrumentation(Runnable runnable) { + Context.current().with(SUPPRESS_INSTRUMENTATION_KEY, true).wrap(runnable).run(); + } + + /** + * Checks if an automatic instrumentation should be suppressed with the provided Context. + * + * @return TRUE to suppress the automatic instrumentation, FALSE to continue with the + * instrumentation. + */ + public static boolean shouldSuppressInstrumentation(Context context) { + return Objects.equals(context.get(SUPPRESS_INSTRUMENTATION_KEY), true); + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/LegacyExporterMetrics.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/LegacyExporterMetrics.java new file mode 100644 index 00000000000..09660485e63 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/LegacyExporterMetrics.java @@ -0,0 +1,196 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Implements health metrics for exporters which were defined prior to the standardization in + * semantic conventions. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class LegacyExporterMetrics implements ExporterMetrics { + + private static final AttributeKey ATTRIBUTE_KEY_TYPE = stringKey("type"); + private static final AttributeKey ATTRIBUTE_KEY_SUCCESS = booleanKey("success"); + + private final Supplier meterProviderSupplier; + private final String exporterName; + private final String transportName; + private final Attributes seenAttrs; + private final Attributes successAttrs; + private final Attributes failedAttrs; + + /** Access via {@link #seen()}. */ + @Nullable private volatile LongCounter seen; + + /** Access via {@link #exported()} . */ + @Nullable private volatile LongCounter exported; + + LegacyExporterMetrics( + Supplier meterProviderSupplier, + StandardComponentId.ExporterType exporterType) { + this.meterProviderSupplier = meterProviderSupplier; + this.exporterName = getExporterName(exporterType); + this.transportName = getTransportName(exporterType); + this.seenAttrs = + Attributes.builder().put(ATTRIBUTE_KEY_TYPE, getTypeString(exporterType.signal())).build(); + this.successAttrs = this.seenAttrs.toBuilder().put(ATTRIBUTE_KEY_SUCCESS, true).build(); + this.failedAttrs = this.seenAttrs.toBuilder().put(ATTRIBUTE_KEY_SUCCESS, false).build(); + } + + public static boolean isSupportedType(StandardComponentId.ExporterType exporterType) { + switch (exporterType) { + case OTLP_GRPC_SPAN_EXPORTER: + case OTLP_HTTP_SPAN_EXPORTER: + case OTLP_HTTP_JSON_SPAN_EXPORTER: + case ZIPKIN_HTTP_SPAN_EXPORTER: + case ZIPKIN_HTTP_JSON_SPAN_EXPORTER: + case OTLP_GRPC_LOG_EXPORTER: + case OTLP_HTTP_LOG_EXPORTER: + case OTLP_HTTP_JSON_LOG_EXPORTER: + case OTLP_GRPC_METRIC_EXPORTER: + case OTLP_HTTP_METRIC_EXPORTER: + case OTLP_HTTP_JSON_METRIC_EXPORTER: + return true; + default: + return false; + } + } + + private static String getTypeString(Signal signal) { + switch (signal) { + case SPAN: + return "span"; + case LOG: + return "log"; + case METRIC: + return "metric"; + case PROFILE: + throw new IllegalArgumentException("Profiles are not supported"); + } + throw new IllegalArgumentException("Unhandled signal type: " + signal); + } + + private static String getExporterName(StandardComponentId.ExporterType exporterType) { + switch (exporterType) { + case OTLP_GRPC_SPAN_EXPORTER: + case OTLP_HTTP_SPAN_EXPORTER: + case OTLP_HTTP_JSON_SPAN_EXPORTER: + case OTLP_GRPC_LOG_EXPORTER: + case OTLP_HTTP_LOG_EXPORTER: + case OTLP_HTTP_JSON_LOG_EXPORTER: + case OTLP_GRPC_METRIC_EXPORTER: + case OTLP_HTTP_METRIC_EXPORTER: + case OTLP_HTTP_JSON_METRIC_EXPORTER: + return "otlp"; + case ZIPKIN_HTTP_SPAN_EXPORTER: + case ZIPKIN_HTTP_JSON_SPAN_EXPORTER: + return "zipkin"; + case OTLP_GRPC_PROFILES_EXPORTER: + throw new IllegalArgumentException("Profiles are not supported"); + } + throw new IllegalArgumentException("Not a supported exporter type: " + exporterType); + } + + private static String getTransportName(StandardComponentId.ExporterType exporterType) { + switch (exporterType) { + case OTLP_GRPC_SPAN_EXPORTER: + case OTLP_GRPC_LOG_EXPORTER: + case OTLP_GRPC_METRIC_EXPORTER: + return "grpc"; + case OTLP_HTTP_SPAN_EXPORTER: + case OTLP_HTTP_LOG_EXPORTER: + case OTLP_HTTP_METRIC_EXPORTER: + case ZIPKIN_HTTP_SPAN_EXPORTER: + return "http"; + case OTLP_HTTP_JSON_SPAN_EXPORTER: + case OTLP_HTTP_JSON_LOG_EXPORTER: + case OTLP_HTTP_JSON_METRIC_EXPORTER: + case ZIPKIN_HTTP_JSON_SPAN_EXPORTER: + return "http-json"; + case OTLP_GRPC_PROFILES_EXPORTER: + throw new IllegalArgumentException("Profiles are not supported"); + } + throw new IllegalArgumentException("Not a supported exporter type: " + exporterType); + } + + /** Record number of records seen. */ + private void addSeen(long value) { + seen().add(value, seenAttrs); + } + + /** Record number of records which successfully exported. */ + private void addSuccess(long value) { + exported().add(value, successAttrs); + } + + /** Record number of records which failed to export. */ + private void addFailed(long value) { + exported().add(value, failedAttrs); + } + + private LongCounter seen() { + LongCounter seen = this.seen; + if (seen == null || SemConvExporterMetrics.isNoop(seen)) { + seen = meter().counterBuilder(exporterName + ".exporter.seen").build(); + this.seen = seen; + } + return seen; + } + + private LongCounter exported() { + LongCounter exported = this.exported; + if (exported == null || SemConvExporterMetrics.isNoop(exported)) { + exported = meter().counterBuilder(exporterName + ".exporter.exported").build(); + this.exported = exported; + } + return exported; + } + + private Meter meter() { + MeterProvider meterProvider = meterProviderSupplier.get(); + if (meterProvider == null) { + meterProvider = MeterProvider.noop(); + } + return meterProvider.get("io.opentelemetry.exporters." + exporterName + "-" + transportName); + } + + @Override + public ExporterMetrics.Recording startRecordingExport(int itemCount) { + return new Recording(itemCount); + } + + private class Recording extends ExporterMetrics.Recording { + + private final int itemCount; + + private Recording(int itemCount) { + this.itemCount = itemCount; + addSeen(itemCount); + } + + @Override + protected void doFinish(@Nullable String errorType, Attributes requestAttributes) { + if (errorType != null) { + addFailed(itemCount); + } else { + addSuccess(itemCount); + } + } + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/NoopExporterMetrics.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/NoopExporterMetrics.java new file mode 100644 index 00000000000..975dbac15a7 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/NoopExporterMetrics.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; + +class NoopExporterMetrics implements ExporterMetrics { + + static final NoopExporterMetrics INSTANCE = new NoopExporterMetrics(); + + @Override + public Recording startRecordingExport(int itemCount) { + return new NoopRecording(); + } + + private static class NoopRecording extends Recording { + + @Override + protected void doFinish(@Nullable String errorType, Attributes requestAttributes) {} + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/README.md b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/README.md new file mode 100644 index 00000000000..5bd237f67ae --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/README.md @@ -0,0 +1,3 @@ +All classes in this package were copied from elsewhere with the project. This was done to eliminate +dependencies on any class in an `*.internal.*` package. See +https://github.com/open-telemetry/opentelemetry-java/issues/7863. diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/RateLimiter.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/RateLimiter.java new file mode 100644 index 00000000000..f72a3b171f1 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/RateLimiter.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.sdk.common.Clock; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class was taken from Jaeger java client. + * https://github.com/jaegertracing/jaeger-client-java/blob/master/jaeger-core/src/main/java/io/jaegertracing/internal/samplers/RateLimitingSampler.java + * + *

Variables have been renamed for clarity. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class RateLimiter { + private final Clock clock; + private final double creditsPerNanosecond; + private final long maxBalance; // max balance in nano ticks + private final AtomicLong currentBalance; // last op nano time less remaining balance + + /** + * Create a new RateLimiter with the provided parameters. + * + * @param creditsPerSecond How many credits to accrue per second. + * @param maxBalance The maximum balance that the limiter can hold, which corresponds to the rate + * that is being limited to. + * @param clock An implementation of the {@link Clock} interface. + */ + public RateLimiter(double creditsPerSecond, double maxBalance, Clock clock) { + this.clock = clock; + this.creditsPerNanosecond = creditsPerSecond / 1.0e9; + this.maxBalance = (long) (maxBalance / creditsPerNanosecond); + this.currentBalance = new AtomicLong(clock.nanoTime() - this.maxBalance); + } + + /** + * Check to see if the provided cost can be spent within the current limits. Will deduct the cost + * from the current balance if it can be spent. + */ + public boolean trySpend(double itemCost) { + long cost = (long) (itemCost / creditsPerNanosecond); + long currentNanos; + long currentBalanceNanos; + long availableBalanceAfterWithdrawal; + do { + currentBalanceNanos = this.currentBalance.get(); + currentNanos = clock.nanoTime(); + long currentAvailableBalance = currentNanos - currentBalanceNanos; + if (currentAvailableBalance > maxBalance) { + currentAvailableBalance = maxBalance; + } + availableBalanceAfterWithdrawal = currentAvailableBalance - cost; + if (availableBalanceAfterWithdrawal < 0) { + return false; + } + } while (!this.currentBalance.compareAndSet( + currentBalanceNanos, currentNanos - availableBalanceAfterWithdrawal)); + return true; + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/SemConvAttributes.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/SemConvAttributes.java new file mode 100644 index 00000000000..214258538de --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/SemConvAttributes.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.api.common.AttributeKey; + +/** + * Provides access to semantic convention attributes used within the SDK implementation. This avoids + * having to pull in semantic conventions as a dependency, which would easily collide and conflict + * with user-provided dependencies. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class SemConvAttributes { + + private SemConvAttributes() {} + + public static final AttributeKey OTEL_COMPONENT_TYPE = + AttributeKey.stringKey("otel.component.type"); + public static final AttributeKey OTEL_COMPONENT_NAME = + AttributeKey.stringKey("otel.component.name"); + public static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("error.type"); + + public static final AttributeKey SERVER_ADDRESS = + AttributeKey.stringKey("server.address"); + public static final AttributeKey SERVER_PORT = AttributeKey.longKey("server.port"); + + public static final AttributeKey RPC_GRPC_STATUS_CODE = + AttributeKey.longKey("rpc.grpc.status_code"); + public static final AttributeKey HTTP_RESPONSE_STATUS_CODE = + AttributeKey.longKey("http.response.status_code"); + + public static final AttributeKey OTEL_SPAN_PARENT_ORIGIN = + AttributeKey.stringKey("otel.span.parent.origin"); + public static final AttributeKey OTEL_SPAN_SAMPLING_RESULT = + AttributeKey.stringKey("otel.span.sampling_result"); +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/SemConvExporterMetrics.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/SemConvExporterMetrics.java new file mode 100644 index 00000000000..13b30b8a2df --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/SemConvExporterMetrics.java @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.Clock; +import java.util.Collections; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class SemConvExporterMetrics implements ExporterMetrics { + + private static final Clock CLOCK = Clock.getDefault(); + + private final Supplier meterProviderSupplier; + private final Signal signal; + private final ComponentId componentId; + private final Attributes additionalAttributes; + + @Nullable private volatile LongUpDownCounter inflight = null; + @Nullable private volatile LongCounter exported = null; + @Nullable private volatile DoubleHistogram duration = null; + @Nullable private volatile Attributes allAttributes = null; + + public SemConvExporterMetrics( + Supplier meterProviderSupplier, + Signal signal, + ComponentId componentId, + Attributes additionalAttributes) { + this.meterProviderSupplier = meterProviderSupplier; + this.componentId = componentId; + this.signal = signal; + this.additionalAttributes = additionalAttributes; + } + + @Override + public ExporterMetrics.Recording startRecordingExport(int itemCount) { + return new Recording(itemCount); + } + + private Meter meter() { + MeterProvider meterProvider = meterProviderSupplier.get(); + if (meterProvider == null) { + meterProvider = MeterProvider.noop(); + } + return meterProvider.get("io.opentelemetry.exporters." + componentId.getTypeName()); + } + + private Attributes allAttributes() { + // attributes are initialized lazily to trigger lazy initialization of the componentId + Attributes allAttributes = this.allAttributes; + if (allAttributes == null) { + AttributesBuilder builder = Attributes.builder(); + builder.put(SemConvAttributes.OTEL_COMPONENT_TYPE, componentId.getTypeName()); + builder.put(SemConvAttributes.OTEL_COMPONENT_NAME, componentId.getComponentName()); + builder.putAll(additionalAttributes); + allAttributes = builder.build(); + this.allAttributes = allAttributes; + } + return allAttributes; + } + + private LongUpDownCounter inflight() { + LongUpDownCounter inflight = this.inflight; + if (inflight == null || isNoop(inflight)) { + String unit = signal.getMetricUnit(); + inflight = + meter() + .upDownCounterBuilder(signal.getExporterMetricNamespace() + ".inflight") + .setUnit("{" + unit + "}") + .setDescription( + "The number of " + + unit + + "s which were passed to the exporter, but that have not been exported yet (neither successful, nor failed)") + .build(); + this.inflight = inflight; + } + return inflight; + } + + private LongCounter exported() { + LongCounter exported = this.exported; + if (exported == null || isNoop(exported)) { + String unit = signal.getMetricUnit(); + exported = + meter() + .counterBuilder(signal.getExporterMetricNamespace() + ".exported") + .setUnit("{" + unit + "}") + .setDescription( + "The number of " + + unit + + "s for which the export has finished, either successful or failed") + .build(); + this.exported = exported; + } + return exported; + } + + private DoubleHistogram duration() { + DoubleHistogram duration = this.duration; + if (duration == null || isNoop(duration)) { + duration = + meter() + .histogramBuilder("otel.sdk.exporter.operation.duration") + .setUnit("s") + .setDescription("The duration of exporting a batch of telemetry records") + .setExplicitBucketBoundariesAdvice(Collections.emptyList()) + .build(); + this.duration = duration; + } + return duration; + } + + private void incrementInflight(long count) { + inflight().add(count, allAttributes()); + } + + private void decrementInflight(long count) { + inflight().add(-count, allAttributes()); + } + + private void incrementExported(long count, @Nullable String errorType) { + exported().add(count, getAttributesWithPotentialError(errorType, Attributes.empty())); + } + + static boolean isNoop(Object instrument) { + // This is a poor way to identify a Noop implementation, but the API doesn't provide a better + // way. Perhaps we could add a common "Noop" interface to allow for an instanceof check? + return instrument.getClass().getSimpleName().startsWith("Noop"); + } + + private Attributes getAttributesWithPotentialError( + @Nullable String errorType, Attributes additionalAttributes) { + Attributes attributes = allAttributes(); + boolean errorPresent = errorType != null && !errorType.isEmpty(); + if (errorPresent || !additionalAttributes.isEmpty()) { + AttributesBuilder builder = attributes.toBuilder(); + if (errorPresent) { + builder.put(SemConvAttributes.ERROR_TYPE, errorType); + } + attributes = builder.putAll(additionalAttributes).build(); + } + return attributes; + } + + private void recordDuration( + double seconds, @Nullable String errorType, Attributes requestAttributes) { + duration().record(seconds, getAttributesWithPotentialError(errorType, requestAttributes)); + } + + private class Recording extends ExporterMetrics.Recording { + + private final int itemCount; + + private final long startNanoTime; + + private Recording(int itemCount) { + this.itemCount = itemCount; + startNanoTime = CLOCK.nanoTime(); + incrementInflight(itemCount); + } + + @Override + protected void doFinish(@Nullable String errorType, Attributes requestAttributes) { + decrementInflight(itemCount); + incrementExported(itemCount, errorType); + long durationNanos = CLOCK.nanoTime() - startNanoTime; + recordDuration(durationNanos / 1_000_000_000.0, errorType, requestAttributes); + } + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/Signal.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/Signal.java new file mode 100644 index 00000000000..8ceae7a8dc5 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/Signal.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import java.util.Locale; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum Signal { + SPAN("otel.sdk.exporter.span", "span"), + METRIC("otel.sdk.exporter.metric_data_point", "data_point"), + LOG("otel.sdk.exporter.log", "log_record"), + PROFILE("TBD", "TBD"); + + private final String exporterMetricNamespace; + private final String metricUnit; + + Signal(String exporterMetricNamespace, String metricUnit) { + this.exporterMetricNamespace = exporterMetricNamespace; + this.metricUnit = metricUnit; + } + + public String logFriendlyName() { + return name().toLowerCase(Locale.ENGLISH); + } + + public String getExporterMetricNamespace() { + return exporterMetricNamespace; + } + + public String getMetricUnit() { + return metricUnit; + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/StandardComponentId.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/StandardComponentId.java new file mode 100644 index 00000000000..f193da80721 --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/StandardComponentId.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +/** + * A {@link ComponentId} where the component type is one of {@link ExporterType}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class StandardComponentId extends ComponentId.Lazy { + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public enum ExporterType { + OTLP_GRPC_SPAN_EXPORTER("otlp_grpc_span_exporter", Signal.SPAN), + OTLP_HTTP_SPAN_EXPORTER("otlp_http_span_exporter", Signal.SPAN), + OTLP_HTTP_JSON_SPAN_EXPORTER("otlp_http_json_span_exporter", Signal.SPAN), + OTLP_GRPC_LOG_EXPORTER("otlp_grpc_log_exporter", Signal.LOG), + OTLP_HTTP_LOG_EXPORTER("otlp_http_log_exporter", Signal.LOG), + OTLP_HTTP_JSON_LOG_EXPORTER("otlp_http_json_log_exporter", Signal.LOG), + OTLP_GRPC_METRIC_EXPORTER("otlp_grpc_metric_exporter", Signal.METRIC), + OTLP_HTTP_METRIC_EXPORTER("otlp_http_metric_exporter", Signal.METRIC), + OTLP_HTTP_JSON_METRIC_EXPORTER("otlp_http_json_metric_exporter", Signal.METRIC), + ZIPKIN_HTTP_SPAN_EXPORTER("zipkin_http_span_exporter", Signal.SPAN), + /** + * Has the same semconv attribute value as ZIPKIN_HTTP_SPAN_EXPORTER, but we still use a + * different enum value for now because they produce separate legacy metrics. + */ + ZIPKIN_HTTP_JSON_SPAN_EXPORTER("zipkin_http_span_exporter", Signal.SPAN), + + OTLP_GRPC_PROFILES_EXPORTER("TBD", Signal.PROFILE); // TODO: not yet standardized in semconv + + final String value; + private final Signal signal; + + ExporterType(String value, Signal signal) { + this.value = value; + this.signal = signal; + } + + public Signal signal() { + return signal; + } + } + + private final ExporterType standardType; + + StandardComponentId(ExporterType standardType) { + super(standardType.value); + this.standardType = standardType; + } + + public ExporterType getStandardType() { + return standardType; + } +} diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ThrottlingLogger.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ThrottlingLogger.java new file mode 100644 index 00000000000..d6ffd23147a --- /dev/null +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/internal/copied/ThrottlingLogger.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import static java.util.concurrent.TimeUnit.MINUTES; + +import io.opentelemetry.sdk.common.Clock; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Will limit the number of log messages emitted, so as not to spam when problems are happening. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class ThrottlingLogger { + private static final double DEFAULT_RATE_LIMIT = 5; + private static final double DEFAULT_THROTTLED_RATE_LIMIT = 1; + private static final TimeUnit DEFAULT_RATE_TIME_UNIT = MINUTES; + + private final Logger delegate; + private final AtomicBoolean throttled = new AtomicBoolean(false); + private final RateLimiter fastRateLimiter; + private final RateLimiter throttledRateLimiter; + + private final double rateLimit; + private final double throttledRateLimit; + private final TimeUnit rateTimeUnit; + + /** Create a new logger which will enforce a max number of messages per minute. */ + public ThrottlingLogger(Logger delegate) { + this(delegate, Clock.getDefault()); + } + + /** Alternate way to create logger that allows setting custom intervals and units. * */ + public ThrottlingLogger( + Logger delegate, double rateLimit, double throttledRateLimit, TimeUnit rateTimeUnit) { + this(delegate, Clock.getDefault(), rateLimit, throttledRateLimit, rateTimeUnit); + } + + // visible for testing + ThrottlingLogger(Logger delegate, Clock clock) { + this(delegate, clock, DEFAULT_RATE_LIMIT, DEFAULT_THROTTLED_RATE_LIMIT, DEFAULT_RATE_TIME_UNIT); + } + + ThrottlingLogger( + Logger delegate, + Clock clock, + double rateLimit, + double throttledRateLimit, + TimeUnit rateTimeUnit) { + this.delegate = delegate; + this.rateLimit = rateLimit; + this.throttledRateLimit = throttledRateLimit; + this.rateTimeUnit = rateTimeUnit; + this.fastRateLimiter = + new RateLimiter(this.rateLimit / this.rateTimeUnit.toSeconds(1), this.rateLimit, clock); + this.throttledRateLimiter = + new RateLimiter( + this.throttledRateLimit / this.rateTimeUnit.toSeconds(1), + this.throttledRateLimit, + clock); + } + + /** Log a message at the given level. */ + public void log(Level level, String message) { + log(level, message, null); + } + + /** Log a message at the given level with a throwable. */ + public void log(Level level, String message, @Nullable Throwable throwable) { + if (!isLoggable(level)) { + return; + } + if (throttled.get()) { + if (throttledRateLimiter.trySpend(1.0)) { + doLog(level, message, throwable); + } + return; + } + + if (fastRateLimiter.trySpend(1.0)) { + doLog(level, message, throwable); + return; + } + + if (throttled.compareAndSet(false, true)) { + // spend the balance in the throttled one, so that it starts at zero. + throttledRateLimiter.trySpend(throttledRateLimit); + String timeUnitString = rateTimeUnit.toString().toLowerCase(Locale.ROOT); + String throttleMessage = + String.format( + Locale.ROOT, + "Too many log messages detected. Will only log %.0f time(s) per %s from now on.", + throttledRateLimit, + timeUnitString.substring(0, timeUnitString.length() - 1)); + delegate.log(level, throttleMessage); + doLog(level, message, throwable); + } + } + + private void doLog(Level level, String message, @Nullable Throwable throwable) { + if (throwable != null) { + delegate.log(level, message, throwable); + } else { + delegate.log(level, message); + } + } + + /** + * Returns whether the current wrapped logger is set to log at the given level. + * + * @return true if the logger set to log at the requested level. + */ + public boolean isLoggable(Level level) { + return delegate.isLoggable(level); + } +} diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java index 4ad9bdfb106..38fb9ee078e 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterEndToEndHttpTest.java @@ -50,6 +50,7 @@ import zipkin2.reporter.SpanBytesEncoder; import zipkin2.reporter.okhttp3.OkHttpSender; +@SuppressWarnings("deprecation") // testing deprecated code @Testcontainers(disabledWithoutDocker = true) class ZipkinSpanExporterEndToEndHttpTest { private static final WebClient client = WebClient.of(); diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java index 759cfae782b..b0cb1ba55e0 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java @@ -15,9 +15,9 @@ import static org.mockito.Mockito.when; import io.github.netmikey.logunit.api.LogCapturer; -import io.opentelemetry.api.internal.InstrumentationUtil; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.context.Context; +import io.opentelemetry.exporter.zipkin.internal.copied.InstrumentationUtil; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InternalTelemetryVersion; @@ -41,6 +41,7 @@ import zipkin2.reporter.SpanBytesEncoder; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("deprecation") // testing deprecated code class ZipkinSpanExporterTest { @Mock private BytesMessageSender mockSender; diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProviderTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProviderTest.java index 289fa1f7709..814d2d967fc 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProviderTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/ZipkinSpanExporterProviderTest.java @@ -7,7 +7,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.util.Collections; @@ -15,6 +14,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; +@SuppressWarnings("deprecation") // testing deprecated code class ZipkinSpanExporterProviderTest { private static final ZipkinSpanExporterProvider provider = new ZipkinSpanExporterProvider(); @@ -28,7 +28,8 @@ void getName() { void createExporter_Default() { try (SpanExporter spanExporter = provider.createExporter(DefaultConfigProperties.createFromMap(Collections.emptyMap()))) { - assertThat(spanExporter).isInstanceOf(ZipkinSpanExporter.class); + assertThat(spanExporter) + .isInstanceOf(io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.class); assertThat(spanExporter) .extracting("sender") .extracting("delegate") @@ -50,7 +51,8 @@ void createExporter_WithConfiguration() { try (SpanExporter spanExporter = provider.createExporter(DefaultConfigProperties.createFromMap(config))) { - assertThat(spanExporter).isInstanceOf(ZipkinSpanExporter.class); + assertThat(spanExporter) + .isInstanceOf(io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.class); assertThat(spanExporter) .extracting("sender") .extracting("delegate") diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterInstrumentationTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterInstrumentationTest.java new file mode 100644 index 00000000000..f7eb70d6df5 --- /dev/null +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/ExporterInstrumentationTest.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InternalTelemetryVersion; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mockito; + +class ExporterInstrumentationTest { + + @SuppressWarnings("unchecked") + Supplier meterProviderSupplier = mock(Supplier.class); + + @ParameterizedTest + @EnumSource + void validMeterProvider(InternalTelemetryVersion schemaVersion) { + when(meterProviderSupplier.get()) + .thenReturn( + SdkMeterProvider.builder() + // Have to provide a valid reader. + .registerMetricReader( + new MetricReader() { + @Override + public void register(CollectionRegistration registration) {} + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public AggregationTemporality getAggregationTemporality( + InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } + }) + .build()); + ExporterInstrumentation instrumentation = + new ExporterInstrumentation( + schemaVersion, + meterProviderSupplier, + ComponentId.generateLazy(StandardComponentId.ExporterType.OTLP_GRPC_SPAN_EXPORTER), + "http://testing:1234"); + verifyNoInteractions(meterProviderSupplier); // Ensure lazy + + // Verify the supplier is only called once per underlying meter. + + instrumentation.startRecordingExport(42).finishFailed("foo"); + instrumentation.startRecordingExport(42).finishSuccessful(); + verify(meterProviderSupplier, atLeastOnce()).get(); + + instrumentation.startRecordingExport(42).finishFailed("foo"); + instrumentation.startRecordingExport(42).finishSuccessful(); + verifyNoMoreInteractions(meterProviderSupplier); + } + + @ParameterizedTest + @EnumSource + void noopMeterProvider(InternalTelemetryVersion schemaVersion) { + + when(meterProviderSupplier.get()).thenReturn(MeterProvider.noop()); + ExporterInstrumentation instrumentation = + new ExporterInstrumentation( + schemaVersion, + meterProviderSupplier, + ComponentId.generateLazy(StandardComponentId.ExporterType.OTLP_GRPC_SPAN_EXPORTER), + "http://testing:1234"); + verifyNoInteractions(meterProviderSupplier); // Ensure lazy + + // Verify the supplier is invoked multiple times since it returns a noop meter. + instrumentation.startRecordingExport(42).finishFailed("foo"); + instrumentation.startRecordingExport(42).finishSuccessful(); + verify(meterProviderSupplier, atLeastOnce()).get(); + + Mockito.clearInvocations((Object) meterProviderSupplier); + instrumentation.startRecordingExport(42).finishFailed("foo"); + instrumentation.startRecordingExport(42).finishSuccessful(); + verify(meterProviderSupplier, atLeastOnce()).get(); + } + + @Test + void serverAttributesInvalidUrl() { + assertThat(ExporterInstrumentation.extractServerAttributes("^")).isEmpty(); + } + + @Test + void serverAttributesEmptyUrl() { + assertThat(ExporterInstrumentation.extractServerAttributes("")).isEmpty(); + } + + @Test + void serverAttributesHttps() { + assertThat(ExporterInstrumentation.extractServerAttributes("https://example.com/foo/bar?a=b")) + .hasSize(2) + .containsEntry(SemConvAttributes.SERVER_ADDRESS, "example.com") + .containsEntry(SemConvAttributes.SERVER_PORT, 443); + + assertThat( + ExporterInstrumentation.extractServerAttributes("https://example.com:1234/foo/bar?a=b")) + .hasSize(2) + .containsEntry(SemConvAttributes.SERVER_ADDRESS, "example.com") + .containsEntry(SemConvAttributes.SERVER_PORT, 1234); + } + + @Test + void serverAttributesHttp() { + assertThat(ExporterInstrumentation.extractServerAttributes("http://example.com/foo/bar?a=b")) + .hasSize(2) + .containsEntry(SemConvAttributes.SERVER_ADDRESS, "example.com") + .containsEntry(SemConvAttributes.SERVER_PORT, 80); + + assertThat( + ExporterInstrumentation.extractServerAttributes("http://example.com:1234/foo/bar?a=b")) + .hasSize(2) + .containsEntry(SemConvAttributes.SERVER_ADDRESS, "example.com") + .containsEntry(SemConvAttributes.SERVER_PORT, 1234); + } + + @Test + void serverAttributesUnknownScheme() { + assertThat(ExporterInstrumentation.extractServerAttributes("custom://foo")) + .hasSize(1) + .containsEntry(SemConvAttributes.SERVER_ADDRESS, "foo"); + + assertThat(ExporterInstrumentation.extractServerAttributes("custom://foo:1234")) + .hasSize(2) + .containsEntry(SemConvAttributes.SERVER_ADDRESS, "foo") + .containsEntry(SemConvAttributes.SERVER_PORT, 1234); + } +} diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/RateLimiterTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/RateLimiterTest.java new file mode 100644 index 00000000000..cace1b8994f --- /dev/null +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/RateLimiterTest.java @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.testing.time.TestClock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +/** + * This class was taken from Jaeger java client. + * https://github.com/jaegertracing/jaeger-client-java/blob/master/jaeger-core/src/test/java/io/jaegertracing/internal/utils/RateLimiterTest.java + */ +class RateLimiterTest { + + @Test + void testRateLimiterWholeNumber() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(2.0, 2.0, clock); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + // move time 250ms forward, not enough credits to pay for 1.0 item + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(250))); + assertThat(limiter.trySpend(1.0)).isFalse(); + + // move time 500ms forward, now enough credits to pay for 1.0 item + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(500))); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + + // move time 5s forward, enough to accumulate credits for 10 messages, but it should still be + // capped at 2 + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(5000))); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + assertThat(limiter.trySpend(1.0)).isFalse(); + assertThat(limiter.trySpend(1.0)).isFalse(); + } + + @Test + void testRateLimiterSteadyRate() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(5.0 / 60.0, 5.0, clock); + for (int i = 0; i < 100; i++) { + assertThat(limiter.trySpend(1.0)).isTrue(); + clock.advance(Duration.ofNanos(TimeUnit.SECONDS.toNanos(20))); + } + } + + @Test + void cantWithdrawMoreThanMax() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(1, 1.0, clock); + assertThat(limiter.trySpend(2)).isFalse(); + } + + @Test + void testRateLimiterLessThanOne() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(0.5, 0.5, clock); + + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isFalse(); + // move time 250ms forward, not enough credits to pay for 1.0 item + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(250))); + assertThat(limiter.trySpend(0.25)).isFalse(); + + // move time 500ms forward, now enough credits to pay for 1.0 item + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(500))); + + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isFalse(); + + // move time 5s forward, enough to accumulate credits for 10 messages, but it should still be + // capped at 2 + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(5000))); + + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isTrue(); + assertThat(limiter.trySpend(0.25)).isFalse(); + assertThat(limiter.trySpend(0.25)).isFalse(); + assertThat(limiter.trySpend(0.25)).isFalse(); + } + + @Test + void testRateLimiterMaxBalance() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(0.1, 1.0, clock); + + clock.advance(Duration.ofNanos(TimeUnit.MICROSECONDS.toNanos(100))); + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + + // move time 20s forward, enough to accumulate credits for 2 messages, but it should still be + // capped at 1 + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(20000))); + + assertThat(limiter.trySpend(1.0)).isTrue(); + assertThat(limiter.trySpend(1.0)).isFalse(); + } + + /** + * Validates rate limiter behavior with {@link System#nanoTime()}-like (non-zero) initial nano + * ticks. + */ + @Test + void testRateLimiterInitial() { + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(1000, 100, clock); + + assertThat(limiter.trySpend(100)).isTrue(); // consume initial (max) balance + assertThat(limiter.trySpend(1)).isFalse(); + + // add 49 credits + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(49))); + assertThat(limiter.trySpend(50)).isFalse(); + + // add one credit + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(1))); + assertThat(limiter.trySpend(50)).isTrue(); // consume accrued balance + assertThat(limiter.trySpend(1)).isFalse(); + + // add a lot of credits (max out balance) + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(1_000_000))); + assertThat(limiter.trySpend(1)).isTrue(); // take one credit + + // add a lot of credits (max out balance) + clock.advance(Duration.ofNanos(TimeUnit.MILLISECONDS.toNanos(1_000_000))); + assertThat(limiter.trySpend(101)).isFalse(); // can't consume more than max balance + assertThat(limiter.trySpend(100)).isTrue(); // consume max balance + assertThat(limiter.trySpend(1)).isFalse(); + } + + /** Validates concurrent credit check correctness. */ + @Test + void testRateLimiterConcurrency() throws InterruptedException, ExecutionException { + int numWorkers = 8; + ExecutorService executorService = Executors.newFixedThreadPool(numWorkers); + int creditsPerWorker = 1000; + TestClock clock = TestClock.create(); + RateLimiter limiter = new RateLimiter(1, numWorkers * creditsPerWorker, clock); + AtomicInteger count = new AtomicInteger(); + List> futures = new ArrayList<>(numWorkers); + for (int w = 0; w < numWorkers; ++w) { + Future future = + executorService.submit( + () -> { + for (int i = 0; i < creditsPerWorker * 2; ++i) { + if (limiter.trySpend(1)) { + count.getAndIncrement(); // count allowed operations + } + } + }); + futures.add(future); + } + for (Future future : futures) { + future.get(); + } + executorService.shutdown(); + executorService.awaitTermination(1, TimeUnit.SECONDS); + assertThat(count.get()) + .withFailMessage("Exactly the allocated number of credits must be consumed") + .isEqualTo(numWorkers * creditsPerWorker); + assertThat(limiter.trySpend(1)).isFalse(); + } +} diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/ThrottlingLoggerTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/ThrottlingLoggerTest.java new file mode 100644 index 00000000000..507ed9be7c0 --- /dev/null +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/internal/copied/ThrottlingLoggerTest.java @@ -0,0 +1,297 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.zipkin.internal.copied; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.INFO; +import static org.slf4j.event.Level.WARN; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.internal.testing.slf4j.SuppressLogger; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.testing.time.TestClock; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressLogger(ThrottlingLoggerTest.class) +class ThrottlingLoggerTest { + + private static final Logger realLogger = Logger.getLogger(ThrottlingLoggerTest.class.getName()); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(ThrottlingLoggerTest.class); + + @Test + void delegation() { + ThrottlingLogger logger = new ThrottlingLogger(realLogger); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.INFO, "oh yes!"); + RuntimeException throwable = new RuntimeException(); + logger.log(Level.SEVERE, "secrets", throwable); + + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(WARN), "oh no!"); + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(INFO), "oh yes!"); + assertThat( + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(ERROR), "secrets") + .getThrowable()) + .isSameAs(throwable); + } + + @Test + void delegationCustom() { + ThrottlingLogger logger = new ThrottlingLogger(realLogger, 10, 2, TimeUnit.HOURS); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.INFO, "oh yes!"); + RuntimeException throwable = new RuntimeException(); + logger.log(Level.SEVERE, "secrets", throwable); + + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(WARN), "oh no!"); + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(INFO), "oh yes!"); + assertThat( + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(ERROR), "secrets") + .getThrowable()) + .isSameAs(throwable); + } + + @Test + void logsBelowLevelDontCount() { + ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(ThrottlingLoggerTest.class.getName())); + + for (int i = 0; i < 100; i++) { + // FINE is below the default level and thus shouldn't impact the rate. + logger.log(Level.FINE, "secrets", new RuntimeException()); + } + logger.log(Level.INFO, "oh yes!"); + + logs.assertContains(loggingEvent -> loggingEvent.getLevel().equals(INFO), "oh yes!"); + } + + @Test + void fiveInAMinuteTriggersLimiting() { + Clock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(7); + logs.assertDoesNotContain("oh no I should be suppressed!"); + logs.assertContains( + "Too many log messages detected. Will only log 1 time(s) per minute from now on."); + logs.assertContains("oh no I should trigger suppression!"); + } + + @Test + void tenInAnHourTriggersLimiting() { + Clock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock, 10, 2, TimeUnit.HOURS); + + for (int i = 0; i < 10; i++) { + logger.log(Level.WARNING, "oh no!"); + } + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(12); + logs.assertDoesNotContain("oh no I should be suppressed!"); + logs.assertContains( + "Too many log messages detected. Will only log 2 time(s) per hour from now on."); + logs.assertContains("oh no I should trigger suppression!"); + } + + @Test + void allowsTrickleOfMessages() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock); + logger.log(Level.WARNING, "oh no!"); + assertThat(logs.size()).isEqualTo(1); + logger.log(Level.WARNING, "oh no!"); + assertThat(logs.size()).isEqualTo(2); + clock.advance(Duration.ofMillis(30_001)); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + assertThat(logs.size()).isEqualTo(4); + + clock.advance(Duration.ofMillis(30_001)); + logger.log(Level.WARNING, "oh no 2nd minute!"); + logger.log(Level.WARNING, "oh no 2nd minute!"); + assertThat(logs.size()).isEqualTo(6); + clock.advance(Duration.ofMillis(30_001)); + logger.log(Level.WARNING, "oh no 2nd minute!"); + logger.log(Level.WARNING, "oh no 2nd minute!"); + assertThat(logs.size()).isEqualTo(8); + + clock.advance(Duration.ofMillis(30_001)); + logger.log(Level.WARNING, "oh no 3rd minute!"); + logger.log(Level.WARNING, "oh no 3rd minute!"); + assertThat(logs.size()).isEqualTo(10); + clock.advance(Duration.ofMillis(30_001)); + logger.log(Level.WARNING, "oh no 3rd minute!"); + logger.log(Level.WARNING, "oh no 3rd minute!"); + assertThat(logs.size()).isEqualTo(12); + } + + @Test + void afterAMinuteLetOneThrough() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(7); + logs.assertDoesNotContain("oh no I should be suppressed!"); + logs.assertContains("oh no I should trigger suppression!"); + logs.assertContains( + "Too many log messages detected. Will only log 1 time(s) per minute from now on."); + + clock.advance(Duration.ofMillis(60_001)); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + assertThat(logs.getEvents()).hasSize(8); + assertThat(logs.getEvents().get(7).getMessage()).isEqualTo("oh no!"); + + clock.advance(Duration.ofMillis(60_001)); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + assertThat(logs.getEvents()).hasSize(9); + assertThat(logs.getEvents().get(8).getMessage()).isEqualTo("oh no!"); + } + + @Test + void afterAnHourLetTwoThrough() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock, 10, 2, TimeUnit.HOURS); + + for (int i = 0; i < 10; i++) { + logger.log(Level.WARNING, "oh no!"); + } + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(12); + logs.assertDoesNotContain("oh no I should be suppressed!"); + logs.assertContains("oh no I should trigger suppression!"); + logs.assertContains( + "Too many log messages detected. Will only log 2 time(s) per hour from now on."); + + clock.advance(Duration.ofMinutes(61)); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + assertThat(logs.getEvents()).hasSize(14); + assertThat(logs.getEvents().get(13).getMessage()).isEqualTo("oh no!"); + + clock.advance(Duration.ofMinutes(61)); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + assertThat(logs.getEvents()).hasSize(16); + assertThat(logs.getEvents().get(15).getMessage()).isEqualTo("oh no!"); + + logs.assertDoesNotContain("oh no I should be suppressed!"); + } + + @Test + void allowOnlyOneLogPerMinuteAfterSuppression() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock); + + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + logger.log(Level.WARNING, "oh no!"); + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(7); + + clock.advance(Duration.ofMillis(12_001)); + logger.log(Level.WARNING, "suppression 1"); + clock.advance(Duration.ofMillis(12_001)); + logger.log(Level.WARNING, "suppression 2"); + clock.advance(Duration.ofMillis(12_001)); + logger.log(Level.WARNING, "suppression 3"); + clock.advance(Duration.ofMillis(12_001)); + logger.log(Level.WARNING, "suppression 4"); + clock.advance(Duration.ofMillis(12_001)); + logger.log(Level.WARNING, "allowed 1"); + + logs.assertDoesNotContain("suppression 1"); + logs.assertDoesNotContain("suppression 2"); + logs.assertDoesNotContain("suppression 3"); + logs.assertDoesNotContain("suppression 4"); + logs.assertContains("allowed 1"); + + assertThat(logs.getEvents()).hasSize(8); + assertThat(logs.getEvents().get(7).getMessage()).isEqualTo("allowed 1"); + } + + @Test + void allowOnlyTwoLogPerHourAfterSuppression() { + TestClock clock = TestClock.create(); + ThrottlingLogger logger = new ThrottlingLogger(realLogger, clock, 10, 2, TimeUnit.HOURS); + + for (int i = 0; i < 10; i++) { + logger.log(Level.WARNING, "oh no!"); + } + + logger.log(Level.WARNING, "oh no I should trigger suppression!"); + logger.log(Level.WARNING, "oh no I should be suppressed!"); + + assertThat(logs.getEvents()).hasSize(12); + + clock.advance(Duration.ofMinutes(10)); + logger.log(Level.WARNING, "suppression 1"); + clock.advance(Duration.ofMinutes(10)); + logger.log(Level.WARNING, "suppression 2"); + clock.advance(Duration.ofMinutes(10)); + clock.advance(Duration.ofSeconds(1)); + logger.log(Level.WARNING, "allowed 1"); + clock.advance(Duration.ofMinutes(10)); + logger.log(Level.WARNING, "suppression 3"); + clock.advance(Duration.ofMinutes(10)); + logger.log(Level.WARNING, "suppression 4"); + clock.advance(Duration.ofMinutes(10)); + clock.advance(Duration.ofSeconds(1)); + logger.log(Level.WARNING, "allowed 2"); + + logs.assertDoesNotContain("suppression 1"); + logs.assertDoesNotContain("suppression 2"); + logs.assertDoesNotContain("suppression 3"); + logs.assertDoesNotContain("suppression 4"); + logs.assertContains("allowed 1"); + logs.assertContains("allowed 2"); + + assertThat(logs.getEvents()).hasSize(14); + assertThat(logs.getEvents().get(12).getMessage()).isEqualTo("allowed 1"); + assertThat(logs.getEvents().get(13).getMessage()).isEqualTo("allowed 2"); + } +} diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java index d2c66386092..69a86641084 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/ConfigurableSpanExporterTest.java @@ -13,7 +13,6 @@ import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import io.opentelemetry.internal.testing.CleanupExtension; import io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; @@ -37,6 +36,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +@SuppressWarnings("deprecation") // testing deprecated code class ConfigurableSpanExporterTest { @RegisterExtension CleanupExtension cleanup = new CleanupExtension(); @@ -168,7 +168,9 @@ void configureSpanProcessors_batchSpanProcessor() { TracerProviderConfiguration.configureSpanProcessors( DefaultConfigProperties.createFromMap( Collections.singletonMap("otel.traces.exporter", exporterName)), - ImmutableMap.of(exporterName, ZipkinSpanExporter.builder().build()), + ImmutableMap.of( + exporterName, + io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.builder().build()), MeterProvider.noop(), closeables); cleanup.addCloseables(closeables); @@ -189,7 +191,7 @@ void configureSpanProcessors_multipleExporters() { "otlp", OtlpGrpcSpanExporter.builder().build(), "zipkin", - ZipkinSpanExporter.builder().build()), + io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.builder().build()), MeterProvider.noop(), closeables); cleanup.addCloseables(closeables); @@ -212,7 +214,8 @@ void configureSpanProcessors_multipleExporters() { spanExporters -> { assertThat(spanExporters.length).isEqualTo(2); assertThat(spanExporters) - .hasAtLeastOneElementOfType(ZipkinSpanExporter.class) + .hasAtLeastOneElementOfType( + io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.class) .hasAtLeastOneElementOfType(OtlpGrpcSpanExporter.class); }); }); @@ -231,7 +234,7 @@ void configureSpanProcessors_multipleExportersWithLogging() { "logging", LoggingSpanExporter.create(), "zipkin", - ZipkinSpanExporter.builder().build()), + io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.builder().build()), MeterProvider.noop(), closeables); cleanup.addCloseables(closeables); diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java index 64e4a55e91a..4d94edb48cc 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/SpanExporterConfigurationTest.java @@ -12,7 +12,6 @@ import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -22,6 +21,7 @@ import java.util.Collections; import org.junit.jupiter.api.Test; +@SuppressWarnings("deprecation") // testing deprecated code class SpanExporterConfigurationTest { private final SpiHelper spiHelper = @@ -42,7 +42,7 @@ void configureExporter_KnownSpiExportersOnClasspath() { assertThat(SpanExporterConfiguration.configureExporter("otlp", spiExportersManager)) .isInstanceOf(OtlpGrpcSpanExporter.class); assertThat(SpanExporterConfiguration.configureExporter("zipkin", spiExportersManager)) - .isInstanceOf(ZipkinSpanExporter.class); + .isInstanceOf(io.opentelemetry.exporter.zipkin.ZipkinSpanExporter.class); } @Test