diff --git a/dd-trace-core/build.gradle b/dd-trace-core/build.gradle index 579e1f679a2..d626672dee4 100644 --- a/dd-trace-core/build.gradle +++ b/dd-trace-core/build.gradle @@ -96,6 +96,7 @@ dependencies { testCompileOnly libs.autoservice.annotation testImplementation project(':dd-java-agent:testing') + testImplementation libs.bundles.mockito testImplementation project(path: ':dd-java-agent:agent-otel:otel-bootstrap', configuration: 'shadow') testImplementation project(':remote-config:remote-config-core') testImplementation project(':products:metrics:metrics-lib') diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/CompositePayloadDispatcherTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/CompositePayloadDispatcherTest.groovy deleted file mode 100644 index 1ad1325719c..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/CompositePayloadDispatcherTest.groovy +++ /dev/null @@ -1,74 +0,0 @@ -package datadog.trace.common.writer - -import datadog.trace.common.writer.ddagent.DDAgentApi -import datadog.trace.common.writer.ddintake.DDIntakeApi -import datadog.trace.core.CoreSpan -import spock.lang.Specification - -class CompositePayloadDispatcherTest extends Specification { - - def "test onDroppedTrace"() { - given: - def dispatcherA = Mock(PayloadDispatcher) - def dispatcherB = Mock(PayloadDispatcher) - def dispatcher = new CompositePayloadDispatcher(dispatcherA, dispatcherB) - - def droppedSpansCount = 1234 - - when: - dispatcher.onDroppedTrace(droppedSpansCount) - - then: - 1 * dispatcherA.onDroppedTrace(droppedSpansCount) - 1 * dispatcherB.onDroppedTrace(droppedSpansCount) - 0 * _ - } - - def "test addTrace"() { - given: - def dispatcherA = Mock(PayloadDispatcher) - def dispatcherB = Mock(PayloadDispatcher) - def dispatcher = new CompositePayloadDispatcher(dispatcherA, dispatcherB) - - def trace = Collections.singletonList(Mock(CoreSpan)) - - when: - dispatcher.addTrace(trace) - - then: - 1 * dispatcherA.addTrace(trace) - 1 * dispatcherB.addTrace(trace) - 0 * _ - } - - def "test flush"() { - given: - def dispatcherA = Mock(PayloadDispatcher) - def dispatcherB = Mock(PayloadDispatcher) - def dispatcher = new CompositePayloadDispatcher(dispatcherA, dispatcherB) - - when: - dispatcher.flush() - - then: - 1 * dispatcherA.flush() - 1 * dispatcherB.flush() - 0 * _ - } - - def "test getApis"() { - given: - def dispatcherA = Mock(PayloadDispatcher) - def dispatcherB = Mock(PayloadDispatcher) - def dispatcher = new CompositePayloadDispatcher(dispatcherA, dispatcherB) - - dispatcherA.getApis() >> [DDIntakeApi] - dispatcherB.getApis() >> [DDAgentApi] - - when: - def apis = dispatcher.getApis() - - then: - apis == [DDIntakeApi, DDAgentApi] - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTelemetryTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTelemetryTest.groovy deleted file mode 100644 index 41fdd0aab53..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTelemetryTest.groovy +++ /dev/null @@ -1,120 +0,0 @@ -package datadog.trace.core.baggage - -import datadog.context.Context -import datadog.trace.api.Config -import datadog.trace.api.metrics.BaggageMetrics -import datadog.trace.api.telemetry.CoreMetricCollector -import spock.lang.Specification - -class BaggagePropagatorTelemetryTest extends Specification { - - def "should directly increment baggage metrics"() { - given: - def baggageMetrics = BaggageMetrics.getInstance() - def collector = CoreMetricCollector.getInstance() - - when: - baggageMetrics.onBaggageInjected() - collector.prepareMetrics() - def metrics = collector.drain() - - then: - def baggageMetric = metrics.find { it.metricName == "context_header_style.injected" } - baggageMetric != null - baggageMetric.value >= 1 - baggageMetric.tags.contains("header_style:baggage") - } - - def "should increment telemetry counter when baggage is successfully extracted"() { - given: - def config = Mock(Config) { - isBaggageExtract() >> true - isBaggageInject() >> true - getBaggageMaxItems() >> 64 - getBaggageMaxBytes() >> 8192 - } - def propagator = new BaggagePropagator(config) - def context = Context.root() - def carrier = ["baggage": "key1=value1,key2=value2"] - def visitor = { map, consumer -> - map.each { k, v -> consumer.accept(k, v) } - } - def collector = CoreMetricCollector.getInstance() - - when: - propagator.extract(context, carrier, visitor) - collector.prepareMetrics() - def metrics = collector.drain() - - then: - def baggageMetric = metrics.find { it.metricName == "context_header_style.extracted" } - baggageMetric != null - baggageMetric.value >= 1 - baggageMetric.tags.contains("header_style:baggage") - } - - def "should directly increment all baggage metrics"() { - given: - def baggageMetrics = BaggageMetrics.getInstance() - def collector = CoreMetricCollector.getInstance() - - when: - baggageMetrics.onBaggageInjected() - baggageMetrics.onBaggageMalformed() - baggageMetrics.onBaggageTruncatedByByteLimit() - baggageMetrics.onBaggageTruncatedByItemLimit() - collector.prepareMetrics() - def metrics = collector.drain() - - then: - def injectedMetric = metrics.find { it.metricName == "context_header_style.injected" } - injectedMetric != null - injectedMetric.value == 1 - injectedMetric.tags.contains("header_style:baggage") - - def malformedMetric = metrics.find { it.metricName == "context_header_style.malformed" } - malformedMetric != null - malformedMetric.value == 1 - malformedMetric.tags.contains("header_style:baggage") - - def bytesTruncatedMetric = metrics.find { - it.metricName == "context_header.truncated" && - it.tags.contains("truncation_reason:baggage_byte_count_exceeded") - } - bytesTruncatedMetric != null - bytesTruncatedMetric.value == 1 - - def itemsTruncatedMetric = metrics.find { - it.metricName == "context_header.truncated" && - it.tags.contains("truncation_reason:baggage_item_count_exceeded") - } - itemsTruncatedMetric != null - itemsTruncatedMetric.value == 1 - } - - def "should not increment telemetry counter when baggage extraction fails"() { - given: - def config = Mock(Config) { - isBaggageExtract() >> true - isBaggageInject() >> true - getBaggageMaxItems() >> 64 - getBaggageMaxBytes() >> 8192 - } - def propagator = new BaggagePropagator(config) - def context = Context.root() - def carrier = [:] // No baggage header - def visitor = { map, consumer -> - map.each { k, v -> consumer.accept(k, v) } - } - def collector = CoreMetricCollector.getInstance() - - when: - propagator.extract(context, carrier, visitor) - collector.prepareMetrics() - def metrics = collector.drain() - - then: - def foundMetrics = metrics.findAll { it.metricName.startsWith("context_header_style.") } - foundMetrics.isEmpty() // No extraction occurred, so no metrics should be created - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/HealthMetricsTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/HealthMetricsTest.groovy deleted file mode 100644 index 500b75a9f5b..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/HealthMetricsTest.groovy +++ /dev/null @@ -1,530 +0,0 @@ -package datadog.trace.core.monitor - -import datadog.metrics.api.statsd.StatsDClient -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.common.writer.RemoteApi -import datadog.trace.common.writer.RemoteWriter -import spock.lang.Ignore -import spock.lang.Specification -import spock.lang.Subject - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ThreadLocalRandom -import java.util.concurrent.TimeUnit - - -class HealthMetricsTest extends Specification { - def statsD = Mock(StatsDClient) - - @Subject - def healthMetrics = new TracerHealthMetrics(statsD) - - // This fails because RemoteWriter isn't an interface and the mock doesn't prevent the call. - @Ignore - def "test onStart"() { - setup: - def writer = Mock(RemoteWriter) - def capacity = ThreadLocalRandom.current().nextInt() - - when: - healthMetrics.onStart(writer) - - then: - 1 * writer.getCapacity() >> capacity - 0 * _ - } - - def "test onShutdown"() { - when: - healthMetrics.onShutdown(true) - - then: - 0 * _ - } - - def "test onPublish"() { - setup: - def latch = new CountDownLatch(trace.isEmpty() ? 1 : 2) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - - when: - healthMetrics.onPublish(trace, samplingPriority) - latch.await(10, TimeUnit.SECONDS) - - then: - 1 * statsD.count('queue.enqueued.traces', 1, "priority:" + priorityName) - (trace.isEmpty() ? 0 : 1) * statsD.count('queue.enqueued.spans', trace.size()) - 0 * _ - cleanup: - healthMetrics.close() - - where: - // spotless:off - trace | samplingPriority | priorityName - [] | PrioritySampling.USER_DROP | "user_drop" - [null, null] | PrioritySampling.USER_DROP | "user_drop" - [] | PrioritySampling.SAMPLER_KEEP | "sampler_keep" - [null, null] | PrioritySampling.SAMPLER_KEEP | "sampler_keep" - // spotless:on - } - - def "test onFailedPublish"() { - setup: - def latch = new CountDownLatch(2) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - - when: - healthMetrics.onFailedPublish(samplingPriority,spanCount) - latch.await(2, TimeUnit.SECONDS) - - then: - 1 * statsD.count('queue.dropped.traces', 1, samplingTag) - 1 * statsD.count('queue.dropped.spans', 1, samplingTag) - 0 * _ - - cleanup: - healthMetrics.close() - - where: - // spotless:off - samplingPriority | samplingTag | spanCount - PrioritySampling.SAMPLER_KEEP | "priority:sampler_keep" | 1 - PrioritySampling.USER_KEEP | "priority:user_keep" | 1 - PrioritySampling.USER_DROP | "priority:user_drop" | 1 - PrioritySampling.SAMPLER_DROP | "priority:sampler_drop" | 1 - PrioritySampling.UNSET | "priority:unset" | 1 - // spotless:off - - - } - - def "test onPartialPublish"() { - setup: - def latch = new CountDownLatch(2) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - - when: - healthMetrics.onPartialPublish(droppedSpans) - latch.await(10, TimeUnit.SECONDS) - - then: - 1 * statsD.count('queue.partial.traces', 1) - 1 * statsD.count('queue.dropped.spans', droppedSpans, samplingPriority) - 0 * _ - - cleanup: - healthMetrics.close() - - where: - // spotless:off - droppedSpans | traces | samplingPriority - 1 | 4 | ['priority:sampler_drop'] - 42 | 1 | ['priority:sampler_drop'] - 3 | 5 | ['priority:sampler_drop'] - // spotless:on - } - - def "test onScheduleFlush"() { - when: - healthMetrics.onScheduleFlush(true) - - then: - 0 * _ - } - - def "test onFlush"() { - when: - healthMetrics.onFlush(true) - - then: - 0 * _ - } - - def "test onSerialize"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - def bytes = ThreadLocalRandom.current().nextInt(10000) - healthMetrics.start() - - when: - healthMetrics.onSerialize(bytes) - latch.await(10, TimeUnit.SECONDS) - - then: - 1 * statsD.count('queue.enqueued.bytes', bytes) - 0 * _ - - cleanup: - healthMetrics.close() - } - - def "test onFailedSerialize"() { - when: - healthMetrics.onFailedSerialize(null, null) - - then: - 0 * _ - } - - def "test onSend #iterationIndex"() { - setup: - def latch = new CountDownLatch(3 + (response.exception().present ? 1 : 0) + (response.status().present ? 1 : 0)) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - - when: - healthMetrics.onSend(traceCount, sendSize, response) - latch.await(10, TimeUnit.SECONDS) - - then: - 1 * statsD.count('api.requests.total', 1) - 1 * statsD.count('flush.traces.total', traceCount) - 1 * statsD.count('flush.bytes.total', sendSize) - if (response.exception().present) { - 1 * statsD.count('api.errors.total', 1) - } - if (response.status().present) { - 1 * statsD.incrementCounter('api.responses.total', ["status:${response.status().asInt}"]) - } - 0 * _ - - cleanup: - healthMetrics.close() - - where: - response << [ - RemoteApi.Response.success(ThreadLocalRandom.current().nextInt(1, 100)), - RemoteApi.Response.failed(ThreadLocalRandom.current().nextInt(1, 100)), - RemoteApi.Response.success(ThreadLocalRandom.current().nextInt(1, 100), new Throwable()), - RemoteApi.Response.failed(new Throwable()), - ] - - traceCount = ThreadLocalRandom.current().nextInt(1, 100) - sendSize = ThreadLocalRandom.current().nextInt(1, 100) - } - - def "test onFailedSend #iterationIndex"() { - setup: - def latch = new CountDownLatch(3 + (response.exception().present ? 1 : 0) + (response.status().present ? 1 : 0)) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - - when: - healthMetrics.onFailedSend(traceCount, sendSize, response) - latch.await(10, TimeUnit.SECONDS) - - then: - 1 * statsD.count('api.requests.total', 1) - 1 * statsD.count('flush.traces.total', traceCount) - 1 * statsD.count('flush.bytes.total', sendSize) - if (response.exception().present) { - 1 * statsD.count('api.errors.total', 1) - } - if (response.status().present) { - 1 * statsD.incrementCounter('api.responses.total', ["status:${response.status().asInt}"]) - } - 0 * _ - - cleanup: - healthMetrics.close() - - where: - response << [ - RemoteApi.Response.success(ThreadLocalRandom.current().nextInt(1, 100)), - RemoteApi.Response.failed(ThreadLocalRandom.current().nextInt(1, 100)), - RemoteApi.Response.success(ThreadLocalRandom.current().nextInt(1, 100), new Throwable()), - RemoteApi.Response.failed(new Throwable()), - ] - - traceCount = ThreadLocalRandom.current().nextInt(1, 100) - sendSize = ThreadLocalRandom.current().nextInt(1, 100) - } - - def "test onCreateTrace"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onCreateTrace() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("trace.pending.created", 1, _) - cleanup: - healthMetrics.close() - } - - def "test onCreateSpan"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onCreateSpan() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("span.pending.created", 1, _) - cleanup: - healthMetrics.close() - } - def "test onCancelContinuation"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onCancelContinuation() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("span.continuations.canceled", 1, _) - cleanup: - healthMetrics.close() - } - def "test onFinishContinuation"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onFinishContinuation() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("span.continuations.finished", 1, _) - cleanup: - healthMetrics.close() - } - def "test onSingleSpanSample"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onSingleSpanSample() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("span.sampling.sampled", 1, _) - cleanup: - healthMetrics.close() - } - def "test onSingleSpanUnsampled"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onSingleSpanUnsampled() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("span.sampling.unsampled", 1, _) - cleanup: - healthMetrics.close() - } - def "test onFinishSpan"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onFinishSpan() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("span.pending.finished", 1, _) - cleanup: - healthMetrics.close() - } - def "test onActivateScope"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onActivateScope() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("scope.activate.count", 1, _) - cleanup: - healthMetrics.close() - } - def "test onCloseScope"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onCloseScope() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("scope.close.count", 1, _) - cleanup: - healthMetrics.close() - } - def "test onScopeCloseError"() { - setup: - def latch = new CountDownLatch(1 + (manual ? 1 : 0)) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onScopeCloseError(manual) - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("scope.close.error", 1, _) - if (manual) { - 1 * statsD.count("scope.user.close.error", 1, _) - } - cleanup: - healthMetrics.close() - where: - manual << [false, true] - } - def "test onScopeStackOverflow"() { - setup: - def latch = new CountDownLatch(1) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onScopeStackOverflow() - latch.await(5, TimeUnit.SECONDS) - then: - 1 * statsD.count("scope.error.stack-overflow", 1, _) - cleanup: - healthMetrics.close() - } - - def "test onLongRunningUpdate"() { - setup: - def latch = new CountDownLatch(3) - def healthMetrics = new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS) - healthMetrics.start() - when: - healthMetrics.onLongRunningUpdate(3,10,1) - latch.await(10, TimeUnit.SECONDS) - then: - 1 * statsD.count("long-running.write", 10, _) - 1 * statsD.count("long-running.dropped", 3, _) - 1 * statsD.count("long-running.expired", 1, _) - cleanup: - healthMetrics.close() - } - - private static class Latched implements StatsDClient { - final StatsDClient delegate - final CountDownLatch latch - - Latched(StatsDClient delegate, CountDownLatch latch) { - this.delegate = delegate - this.latch = latch - } - - @Override - void incrementCounter(String metricName, String... tags) { - try { - delegate.incrementCounter(metricName, tags) - } finally { - latch.countDown() - } - } - - @Override - void count(String metricName, long delta, String... tags) { - try { - delegate.count(metricName, delta, tags) - } finally { - latch.countDown() - } - } - - @Override - void gauge(String metricName, long value, String... tags) { - try { - delegate.gauge(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void gauge(String metricName, double value, String... tags) { - try { - delegate.gauge(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void histogram(String metricName, long value, String... tags) { - try { - delegate.histogram(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void histogram(String metricName, double value, String... tags) { - try { - delegate.histogram(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void distribution(String metricName, long value, String... tags) { - try { - delegate.distribution(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void distribution(String metricName, double value, String... tags) { - try { - delegate.distribution(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void serviceCheck(String serviceCheckName, String status, String message, String... tags) { - try { - delegate.serviceCheck(serviceCheckName, status, message, tags) - } finally { - latch.countDown() - } - } - - @Override - void error(Exception error) { - try { - delegate.error(error) - } finally { - latch.countDown() - } - } - - @Override - int getErrorCount() { - try { - return delegate.getErrorCount() - } finally { - latch.countDown() - } - } - - @Override - void close() { - try { - delegate.close() - } finally { - latch.countDown() - } - } - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy deleted file mode 100644 index ca43d9b6236..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.groovy +++ /dev/null @@ -1,111 +0,0 @@ -package datadog.trace.core.tagprocessor - -import datadog.trace.api.TagMap -import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks -import datadog.trace.core.DDSpanContext -import datadog.trace.api.endpoint.EndpointResolver -import spock.lang.Specification - -class HttpEndpointPostProcessorTest extends Specification { - - def "should not overwrite resource name when http.route is available and eligible"() { - // RFC-1051: the processor enriches stats buckets and tags the span with http.endpoint, - // but must NOT overwrite the span's resourceName — that is the backend's responsibility. - given: - def endpointResolver = new EndpointResolver(true, false) - def processor = new HttpEndpointPostProcessor(endpointResolver) - def mockContext = Mock(DDSpanContext) - def mockSpanLinks = Mock(AppendableSpanLinks) - def tags = TagMap.fromMap([ - (Tags.HTTP_METHOD): "GET", - (Tags.HTTP_ROUTE): "/greeting", - (Tags.HTTP_URL): "http://localhost:8080/greeting" - ]) - - when: - processor.processTags(tags, mockContext, mockSpanLinks) - - then: - 0 * mockContext.setResourceName(_, _) - // http.route is eligible — no http.endpoint tag should be added - !tags.containsKey(Tags.HTTP_ENDPOINT) - } - - def "should compute and tag http.endpoint from URL when route is invalid, without touching resourceName"() { - given: - def endpointResolver = new EndpointResolver(true, false) - def processor = new HttpEndpointPostProcessor(endpointResolver) - def mockContext = Mock(DDSpanContext) - def mockSpanLinks = Mock(AppendableSpanLinks) - def tags = TagMap.fromMap([ - (Tags.HTTP_METHOD): "GET", - (Tags.HTTP_ROUTE): "*", // catch-all — ineligible per RFC-1051 - (Tags.HTTP_URL): "http://localhost:8080/users/123/orders/456" - ]) - - when: - processor.processTags(tags, mockContext, mockSpanLinks) - - then: - 0 * mockContext.setResourceName(_, _) - tags[Tags.HTTP_ENDPOINT] == "/users/{param:int}/orders/{param:int}" - } - - def "should skip non-HTTP spans"() { - given: - def endpointResolver = new EndpointResolver(true, false) - def processor = new HttpEndpointPostProcessor(endpointResolver) - def mockContext = Mock(DDSpanContext) - def mockSpanLinks = Mock(AppendableSpanLinks) - def tags = TagMap.fromMap([ - "db.statement": "SELECT * FROM users" - ]) - - when: - processor.processTags(tags, mockContext, mockSpanLinks) - - then: - 0 * mockContext.setResourceName(_, _) - !tags.containsKey(Tags.HTTP_ENDPOINT) - } - - def "should not process when resource renaming is disabled"() { - given: - def endpointResolver = new EndpointResolver(false, false) - def processor = new HttpEndpointPostProcessor(endpointResolver) - def mockContext = Mock(DDSpanContext) - def mockSpanLinks = Mock(AppendableSpanLinks) - def tags = TagMap.fromMap([ - (Tags.HTTP_METHOD): "GET", - (Tags.HTTP_ROUTE): "/greeting" - ]) - - when: - processor.processTags(tags, mockContext, mockSpanLinks) - - then: - 0 * mockContext.setResourceName(_, _) - !tags.containsKey(Tags.HTTP_ENDPOINT) - } - - def "should tag http.endpoint from URL even when alwaysSimplified is true, without touching resourceName"() { - given: - def endpointResolver = new EndpointResolver(true, true) - def processor = new HttpEndpointPostProcessor(endpointResolver) - def mockContext = Mock(DDSpanContext) - def mockSpanLinks = Mock(AppendableSpanLinks) - def tags = TagMap.fromMap([ - (Tags.HTTP_METHOD): "GET", - (Tags.HTTP_ROUTE): "/greeting", - (Tags.HTTP_URL): "http://localhost:8080/users/123" - ]) - - when: - processor.processTags(tags, mockContext, mockSpanLinks) - - then: - 0 * mockContext.setResourceName(_, _) - tags[Tags.HTTP_ENDPOINT] == "/users/{param:int}" - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/util/StackTracesTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/util/StackTracesTest.groovy deleted file mode 100644 index 646956708ff..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/util/StackTracesTest.groovy +++ /dev/null @@ -1,188 +0,0 @@ -package datadog.trace.core.util - - -import spock.lang.Specification - -class StackTracesTest extends Specification { - - def "test stack trace truncation: #limit"() { - given: - def trace = """ -Exception in thread "main" com.example.app.MainException: Unexpected application failure - at com.example.app.Application\$Runner.run(Application.java:102) - at com.example.app.Application.lambda\$start\$0(Application.java:75) - at java.base/java.util.Optional.ifPresent(Optional.java:178) - at com.example.app.Application.start(Application.java:74) - at com.example.app.Main.main(Main.java:21) - at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) - at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.base/java.lang.reflect.Method.invoke(Method.java:566) - at com.example.launcher.Bootstrap.run(Bootstrap.java:39) - at com.example.launcher.Bootstrap.main(Bootstrap.java:25) - at com.example.internal.\$Proxy1.start(Unknown Source) - at com.example.internal.Initializer\$1.run(Initializer.java:47) - at com.example.internal.Initializer.lambda\$init\$0(Initializer.java:38) - at java.base/java.util.concurrent.Executors\$RunnableAdapter.call(Executors.java:515) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) - at java.base/java.util.concurrent.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:628) - at java.base/java.lang.Thread.run(Thread.java:834) - at com.example.synthetic.Helper.access\$100(Helper.java:14) -Caused by: com.example.db.DatabaseException: Failed to load user data - at com.example.db.UserDao.findUser(UserDao.java:88) - at com.example.db.UserDao.lambda\$cacheLookup\$1(UserDao.java:64) - at com.example.cache.Cache\$Entry.computeIfAbsent(Cache.java:111) - at com.example.cache.Cache.get(Cache.java:65) - at com.example.service.UserService.loadUser(UserService.java:42) - at com.example.service.UserService.lambda\$loadUserAsync\$0(UserService.java:36) - at com.example.util.SafeRunner.run(SafeRunner.java:27) - at java.base/java.util.concurrent.Executors\$RunnableAdapter.call(Executors.java:515) - at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) - at java.base/java.util.concurrent.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:628) - at java.base/java.lang.Thread.run(Thread.java:834) - at com.example.synthetic.UserDao\$1.run(UserDao.java:94) - at com.example.synthetic.UserDao\$1.run(UserDao.java:94) - at com.example.db.ConnectionManager.getConnection(ConnectionManager.java:55) -Suppressed: java.io.IOException: Resource cleanup failed - at com.example.util.ResourceManager.close(ResourceManager.java:23) - at com.example.service.UserService.lambda\$loadUserAsync\$0(UserService.java:38) - ... 3 more -Caused by: java.nio.file.AccessDeniedException: /data/user/config.json - at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90) - at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111) - at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116) - at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219) - at java.base/java.nio.file.Files.newByteChannel(Files.java:375) - at java.base/java.nio.file.Files.newInputStream(Files.java:489) - at com.example.util.FileUtils.readFile(FileUtils.java:22) - at com.example.util.ResourceManager.close(ResourceManager.java:21) - ... 3 more -""" - - expect: - StackTraces.truncate(trace, limit) == expected - - where: - limit | expected - 1000 | """ -Exception in thread "main" com.example.app.MainException: Unexpected application failure - at c.e.a.Application\$Runner.run(Application.java:102) - at c.e.a.Application.lambda\$start\$0(Application.java:75) - at j.b.u.Optional.ifPresent(Optional.java:178) - at c.e.a.Application.start(Application.java:74) - at c.e.a.Main.main(Main.java:21) - at s.r.NativeMethodAccessorImpl.invoke0(Native Method) - at s.r.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) - at s.r.Delegat - ... trace centre-cut to 1000 chars ... -ToIOException(UnixException.java:90) - at j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:111) - at j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:116) - at j.b.n.f.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219) - at j.b.n.f.Files.newByteChannel(Files.java:375) - at j.b.n.f.Files.newInputStream(Files.java:489) - at c.e.u.FileUtils.readFile(FileUtils.java:22) - at c.e.u.ResourceManager.close(ResourceManager.java:21) - ... 3 more -""" - 2500 | """ -Exception in thread "main" com.example.app.MainException: Unexpected application failure - at c.e.a.Application\$Runner.run(Application.java:102) - at c.e.a.Application.lambda\$start\$0(Application.java:75) - at j.b.u.Optional.ifPresent(Optional.java:178) - at c.e.a.Application.start(Application.java:74) - at c.e.a.Main.main(Main.java:21) - at s.r.NativeMethodAccessorImpl.invoke0(Native Method) - at s.r.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) - at s.r.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - ... 8 trimmed ... - at j.b.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) - at j.b.u.c.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:628) - at j.b.l.Thread.run(Thread.java:834) - at c.e.s.Helper.access\$100(Helper.java:14) -Caused by: com.example.db.DatabaseException: Failed to load user data - at c.e.d.UserDao.findUser(UserDao.java:88) - at c.e.d.UserDao.lambda\$cacheLookup\$1(UserDao.java:64) - at c.e.c.Cache\$Entry.computeIfAbsent(Cache.java:111) - at c.e.c.Cache.get(Cache.java:65) - at c.e.s.UserService.loadUser(UserService.java:42) - at c.e.s.UserService.lambda\$loadUserAsync\$0(UserService.java:36) - at c.e.u.SafeRunner.run(SafeRunner.java:27) - at j.b.u.c.Executors\$RunnableAdapter.call(Executors.java:515) - ... 3 trimmed ... - at j.b.l.Thread.run(Thread.java:834) - at c.e.s.UserDao\$1.run(UserDao.java:94) - at c.e.s.UserDao\$1.run(UserDao.java:94) - at c.e.d.ConnectionManager.getConnection(ConnectionManager.java:55) -Suppressed: java.io.IOException: Resource cleanup failed - at c.e.u.ResourceManager.close(ResourceManager.java:23) - at c.e.s.UserService.lambda\$loadUserAsync\$0(UserService.java:38) - ... 3 more -Caused by: java.nio.file.AccessDeniedException: /data/user/config.json - at j.b.n.f.UnixException.translateToIOException(UnixException.java:90) - at j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:111) - at j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:116) - at j.b.n.f.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219) - at j.b.n.f.Files.newByteChannel(Files.java:375) - at j.b.n.f.Files.newInputStream(Files.java:489) - at c.e.u.FileUtils.readFile(FileUtils.java:22) - at c.e.u.ResourceManager.close(ResourceManager.java:21) - ... 3 more -""" - 3000 | """ -Exception in thread "main" com.example.app.MainException: Unexpected application failure - at c.e.a.Application\$Runner.run(Application.java:102) - at c.e.a.Application.lambda\$start\$0(Application.java:75) - at j.b.u.Optional.ifPresent(Optional.java:178) - at c.e.a.Application.start(Application.java:74) - at c.e.a.Main.main(Main.java:21) - at s.r.NativeMethodAccessorImpl.invoke0(Native Method) - at s.r.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) - at s.r.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at j.b.l.r.Method.invoke(Method.java:566) - at c.e.l.Bootstrap.run(Bootstrap.java:39) - at c.e.l.Bootstrap.main(Bootstrap.java:25) - at c.e.i.\$Proxy1.start(Unknown Source) - at c.e.i.Initializer\$1.run(Initializer.java:47) - at c.e.i.Initializer.lambda\$init\$0(Initializer.java:38) - at j.b.u.c.Executors\$RunnableAdapter.call(Executors.java:515) - at j.b.u.c.FutureTask.run(FutureTask.java:264) - at j.b.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) - at j.b.u.c.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:628) - at j.b.l.Thread.run(Thread.java:834) - at c.e.s.Helper.access\$100(Helper.java:14) -Caused by: com.example.db.DatabaseException: Failed to load user data - at c.e.d.UserDao.findUser(UserDao.java:88) - at c.e.d.UserDao.lambda\$cacheLookup\$1(UserDao.java:64) - at c.e.c.Cache\$Entry.computeIfAbsent(Cache.java:111) - at c.e.c.Cache.get(Cache.java:65) - at c.e.s.UserService.loadUser(UserService.java:42) - at c.e.s.UserService.lambda\$loadUserAsync\$0(UserService.java:36) - at c.e.u.SafeRunner.run(SafeRunner.java:27) - at j.b.u.c.Executors\$RunnableAdapter.call(Executors.java:515) - at j.b.u.c.FutureTask.run(FutureTask.java:264) - at j.b.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) - at j.b.u.c.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:628) - at j.b.l.Thread.run(Thread.java:834) - at c.e.s.UserDao\$1.run(UserDao.java:94) - at c.e.s.UserDao\$1.run(UserDao.java:94) - at c.e.d.ConnectionManager.getConnection(ConnectionManager.java:55) -Suppressed: java.io.IOException: Resource cleanup failed - at c.e.u.ResourceManager.close(ResourceManager.java:23) - at c.e.s.UserService.lambda\$loadUserAsync\$0(UserService.java:38) - ... 3 more -Caused by: java.nio.file.AccessDeniedException: /data/user/config.json - at j.b.n.f.UnixException.translateToIOException(UnixException.java:90) - at j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:111) - at j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:116) - at j.b.n.f.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219) - at j.b.n.f.Files.newByteChannel(Files.java:375) - at j.b.n.f.Files.newInputStream(Files.java:489) - at c.e.u.FileUtils.readFile(FileUtils.java:22) - at c.e.u.ResourceManager.close(ResourceManager.java:21) - ... 3 more -""" - } -} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/CompositePayloadDispatcherTest.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/CompositePayloadDispatcherTest.java new file mode 100644 index 00000000000..17eb060e81d --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/CompositePayloadDispatcherTest.java @@ -0,0 +1,77 @@ +package datadog.trace.common.writer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import datadog.trace.core.CoreSpan; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CompositePayloadDispatcherTest { + + @Mock PayloadDispatcher dispatcherA; + @Mock PayloadDispatcher dispatcherB; + + @Test + void testOnDroppedTrace() { + CompositePayloadDispatcher dispatcher = + new CompositePayloadDispatcher(dispatcherA, dispatcherB); + int droppedSpansCount = 1234; + + dispatcher.onDroppedTrace(droppedSpansCount); + + verify(dispatcherA).onDroppedTrace(droppedSpansCount); + verify(dispatcherB).onDroppedTrace(droppedSpansCount); + verifyNoMoreInteractions(dispatcherA, dispatcherB); + } + + @Test + @SuppressWarnings("unchecked") + void testAddTrace() { + CompositePayloadDispatcher dispatcher = + new CompositePayloadDispatcher(dispatcherA, dispatcherB); + List> trace = Collections.singletonList(mock(CoreSpan.class)); + + dispatcher.addTrace(trace); + + verify(dispatcherA).addTrace(trace); + verify(dispatcherB).addTrace(trace); + verifyNoMoreInteractions(dispatcherA, dispatcherB); + } + + @Test + void testFlush() { + CompositePayloadDispatcher dispatcher = + new CompositePayloadDispatcher(dispatcherA, dispatcherB); + + dispatcher.flush(); + + verify(dispatcherA).flush(); + verify(dispatcherB).flush(); + verifyNoMoreInteractions(dispatcherA, dispatcherB); + } + + @Test + void testGetApis() { + CompositePayloadDispatcher dispatcher = + new CompositePayloadDispatcher(dispatcherA, dispatcherB); + RemoteApi apiA = mock(RemoteApi.class); + RemoteApi apiB = mock(RemoteApi.class); + when(dispatcherA.getApis()).thenReturn(Collections.singletonList(apiA)); + when(dispatcherB.getApis()).thenReturn(Collections.singletonList(apiB)); + + Collection apis = dispatcher.getApis(); + + assertEquals(Arrays.asList(apiA, apiB), apis); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/baggage/BaggagePropagatorTelemetryTest.java b/dd-trace-core/src/test/java/datadog/trace/core/baggage/BaggagePropagatorTelemetryTest.java new file mode 100644 index 00000000000..4e125881b43 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/baggage/BaggagePropagatorTelemetryTest.java @@ -0,0 +1,145 @@ +package datadog.trace.core.baggage; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.context.Context; +import datadog.context.propagation.CarrierVisitor; +import datadog.trace.api.Config; +import datadog.trace.api.metrics.BaggageMetrics; +import datadog.trace.api.telemetry.CoreMetricCollector; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class BaggagePropagatorTelemetryTest { + + private static final CarrierVisitor> MAP_VISITOR = + (map, consumer) -> map.forEach(consumer); + + @Test + void shouldDirectlyIncrementBaggageMetrics() { + BaggageMetrics baggageMetrics = BaggageMetrics.getInstance(); + CoreMetricCollector collector = CoreMetricCollector.getInstance(); + + baggageMetrics.onBaggageInjected(); + collector.prepareMetrics(); + Collection metrics = collector.drain(); + + CoreMetricCollector.CoreMetric baggageMetric = + metrics.stream() + .filter(m -> "context_header_style.injected".equals(m.metricName)) + .findFirst() + .orElse(null); + assertNotNull(baggageMetric); + assertTrue(baggageMetric.value.longValue() >= 1); + assertTrue(baggageMetric.tags.contains("header_style:baggage")); + } + + @Test + void shouldIncrementTelemetryCounterWhenBaggageIsSuccessfullyExtracted() { + Config config = mock(Config.class); + when(config.isBaggageExtract()).thenReturn(true); + when(config.isBaggageInject()).thenReturn(true); + when(config.getTraceBaggageMaxItems()).thenReturn(64); + when(config.getTraceBaggageMaxBytes()).thenReturn(8192); + BaggagePropagator propagator = new BaggagePropagator(config); + Context context = Context.root(); + Map carrier = Collections.singletonMap("baggage", "key1=value1,key2=value2"); + CoreMetricCollector collector = CoreMetricCollector.getInstance(); + + propagator.extract(context, carrier, MAP_VISITOR); + collector.prepareMetrics(); + Collection metrics = collector.drain(); + + CoreMetricCollector.CoreMetric baggageMetric = + metrics.stream() + .filter(m -> "context_header_style.extracted".equals(m.metricName)) + .findFirst() + .orElse(null); + assertNotNull(baggageMetric); + assertTrue(baggageMetric.value.longValue() >= 1); + assertTrue(baggageMetric.tags.contains("header_style:baggage")); + } + + @Test + void shouldDirectlyIncrementAllBaggageMetrics() { + BaggageMetrics baggageMetrics = BaggageMetrics.getInstance(); + CoreMetricCollector collector = CoreMetricCollector.getInstance(); + + baggageMetrics.onBaggageInjected(); + baggageMetrics.onBaggageMalformed(); + baggageMetrics.onBaggageTruncatedByByteLimit(); + baggageMetrics.onBaggageTruncatedByItemLimit(); + collector.prepareMetrics(); + Collection metrics = collector.drain(); + + CoreMetricCollector.CoreMetric injectedMetric = + metrics.stream() + .filter(m -> "context_header_style.injected".equals(m.metricName)) + .findFirst() + .orElse(null); + assertNotNull(injectedMetric); + assertTrue(injectedMetric.value.longValue() == 1); + assertTrue(injectedMetric.tags.contains("header_style:baggage")); + + CoreMetricCollector.CoreMetric malformedMetric = + metrics.stream() + .filter(m -> "context_header_style.malformed".equals(m.metricName)) + .findFirst() + .orElse(null); + assertNotNull(malformedMetric); + assertTrue(malformedMetric.value.longValue() == 1); + assertTrue(malformedMetric.tags.contains("header_style:baggage")); + + CoreMetricCollector.CoreMetric bytesTruncatedMetric = + metrics.stream() + .filter( + m -> + "context_header.truncated".equals(m.metricName) + && m.tags.contains("truncation_reason:baggage_byte_count_exceeded")) + .findFirst() + .orElse(null); + assertNotNull(bytesTruncatedMetric); + assertTrue(bytesTruncatedMetric.value.longValue() == 1); + + CoreMetricCollector.CoreMetric itemsTruncatedMetric = + metrics.stream() + .filter( + m -> + "context_header.truncated".equals(m.metricName) + && m.tags.contains("truncation_reason:baggage_item_count_exceeded")) + .findFirst() + .orElse(null); + assertNotNull(itemsTruncatedMetric); + assertTrue(itemsTruncatedMetric.value.longValue() == 1); + } + + @Test + void shouldNotIncrementTelemetryCounterWhenBaggageExtractionFails() { + Config config = mock(Config.class); + when(config.isBaggageExtract()).thenReturn(true); + when(config.isBaggageInject()).thenReturn(true); + when(config.getTraceBaggageMaxItems()).thenReturn(64); + when(config.getTraceBaggageMaxBytes()).thenReturn(8192); + BaggagePropagator propagator = new BaggagePropagator(config); + Context context = Context.root(); + Map carrier = Collections.emptyMap(); + CoreMetricCollector collector = CoreMetricCollector.getInstance(); + + propagator.extract(context, carrier, MAP_VISITOR); + collector.prepareMetrics(); + Collection metrics = collector.drain(); + + List foundMetrics = + metrics.stream() + .filter(m -> m.metricName.startsWith("context_header_style.")) + .collect(Collectors.toList()); + assertTrue(foundMetrics.isEmpty()); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/monitor/HealthMetricsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/monitor/HealthMetricsTest.java new file mode 100644 index 00000000000..d04c674570d --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/monitor/HealthMetricsTest.java @@ -0,0 +1,506 @@ +package datadog.trace.core.monitor; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import datadog.metrics.api.statsd.StatsDClient; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.common.writer.RemoteApi; +import datadog.trace.core.DDSpan; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.tabletest.junit.TableTest; + +@ExtendWith(MockitoExtension.class) +class HealthMetricsTest { + + @Mock StatsDClient statsD; + + @Test + void testOnShutdown() { + TracerHealthMetrics healthMetrics = new TracerHealthMetrics(statsD); + healthMetrics.onShutdown(true); + verifyNoInteractions(statsD); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testOnPublishArguments") + void testOnPublish(String scenario, List trace, int samplingPriority, String priorityName) + throws InterruptedException { + CountDownLatch latch = new CountDownLatch(trace.isEmpty() ? 1 : 2); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onPublish(trace, samplingPriority); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + verify(statsD).count("queue.enqueued.traces", 1L, "priority:" + priorityName); + if (!trace.isEmpty()) { + verify(statsD).count("queue.enqueued.spans", (long) trace.size()); + } + verifyNoMoreInteractions(statsD); + } + + static Stream testOnPublishArguments() { + List emptyTrace = Collections.emptyList(); + List twoSpanTrace = Arrays.asList(null, null); + return Stream.of( + arguments( + "empty trace user_drop", emptyTrace, (int) PrioritySampling.USER_DROP, "user_drop"), + arguments( + "two span trace user_drop", + twoSpanTrace, + (int) PrioritySampling.USER_DROP, + "user_drop"), + arguments( + "empty trace sampler_keep", + emptyTrace, + (int) PrioritySampling.SAMPLER_KEEP, + "sampler_keep"), + arguments( + "two span trace sampler_keep", + twoSpanTrace, + (int) PrioritySampling.SAMPLER_KEEP, + "sampler_keep")); + } + + @TableTest({ + "scenario | samplingPriority | samplingTag ", + "sampler_keep | 1 | priority:sampler_keep", + "user_keep | 2 | priority:user_keep ", + "user_drop | -1 | priority:user_drop ", + "sampler_drop | 0 | priority:sampler_drop", + "unset | -128 | priority:unset " + }) + @ParameterizedTest(name = "testOnFailedPublish [{index}]") + void testOnFailedPublish(int samplingPriority, String samplingTag) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(2); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onFailedPublish(samplingPriority, 1); + assertTrue(latch.await(2, TimeUnit.SECONDS)); + } + verify(statsD).count("queue.dropped.traces", 1L, samplingTag); + verify(statsD).count("queue.dropped.spans", 1L, samplingTag); + verifyNoMoreInteractions(statsD); + } + + @TableTest({ + "scenario | droppedSpans", + "1 dropped | 1 ", + "42 dropped | 42 ", + "3 dropped | 3 " + }) + @ParameterizedTest(name = "testOnPartialPublish [{index}]") + void testOnPartialPublish(int droppedSpans) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(2); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onPartialPublish(droppedSpans); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + verify(statsD).count("queue.partial.traces", 1L); + verify(statsD).count("queue.dropped.spans", (long) droppedSpans, "priority:sampler_drop"); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnScheduleFlush() { + TracerHealthMetrics healthMetrics = new TracerHealthMetrics(statsD); + healthMetrics.onScheduleFlush(true); + verifyNoInteractions(statsD); + } + + @Test + void testOnFlush() { + TracerHealthMetrics healthMetrics = new TracerHealthMetrics(statsD); + healthMetrics.onFlush(true); + verifyNoInteractions(statsD); + } + + @Test + void testOnSerialize() throws InterruptedException { + int bytes = ThreadLocalRandom.current().nextInt(10000); + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onSerialize(bytes); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + verify(statsD).count("queue.enqueued.bytes", (long) bytes); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnFailedSerialize() { + TracerHealthMetrics healthMetrics = new TracerHealthMetrics(statsD); + healthMetrics.onFailedSerialize(null, null); + verifyNoInteractions(statsD); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testOnSendArguments") + void testOnSend(String scenario, RemoteApi.Response response) throws InterruptedException { + int traceCount = ThreadLocalRandom.current().nextInt(1, 100); + int sendSize = ThreadLocalRandom.current().nextInt(1, 100); + int latchCount = + 3 + (response.exception().isPresent() ? 1 : 0) + (response.status().isPresent() ? 1 : 0); + CountDownLatch latch = new CountDownLatch(latchCount); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onSend(traceCount, sendSize, response); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + verifySendAttempt(response, traceCount, sendSize); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testOnFailedSendArguments") + void testOnFailedSend(String scenario, RemoteApi.Response response) throws InterruptedException { + int traceCount = ThreadLocalRandom.current().nextInt(1, 100); + int sendSize = ThreadLocalRandom.current().nextInt(1, 100); + int latchCount = + 3 + (response.exception().isPresent() ? 1 : 0) + (response.status().isPresent() ? 1 : 0); + CountDownLatch latch = new CountDownLatch(latchCount); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onFailedSend(traceCount, sendSize, response); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + verifySendAttempt(response, traceCount, sendSize); + } + + private void verifySendAttempt(RemoteApi.Response response, int traceCount, int sendSize) { + verify(statsD).count("api.requests.total", 1L); + verify(statsD).count("flush.traces.total", (long) traceCount); + verify(statsD).count("flush.bytes.total", (long) sendSize); + if (response.exception().isPresent()) { + verify(statsD).count("api.errors.total", 1L); + } + if (response.status().isPresent()) { + verify(statsD) + .incrementCounter("api.responses.total", "status:" + response.status().getAsInt()); + } + verifyNoMoreInteractions(statsD); + } + + static Stream testOnSendArguments() { + return Stream.of( + arguments( + "success with status", + RemoteApi.Response.success(ThreadLocalRandom.current().nextInt(1, 100))), + arguments( + "failed with status", + RemoteApi.Response.failed(ThreadLocalRandom.current().nextInt(1, 100))), + arguments( + "success with status and exception", + RemoteApi.Response.success( + ThreadLocalRandom.current().nextInt(1, 100), new Throwable())), + arguments("failed with exception", RemoteApi.Response.failed(new Throwable()))); + } + + static Stream testOnFailedSendArguments() { + return testOnSendArguments(); + } + + @Test + void testOnCreateTrace() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onCreateTrace(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("trace.pending.created", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnCreateSpan() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onCreateSpan(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("span.pending.created", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnCancelContinuation() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onCancelContinuation(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("span.continuations.canceled", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnFinishContinuation() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onFinishContinuation(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("span.continuations.finished", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnSingleSpanSample() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onSingleSpanSample(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("span.sampling.sampled", 1L, "sampler:single-span"); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnSingleSpanUnsampled() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onSingleSpanUnsampled(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("span.sampling.unsampled", 1L, "sampler:single-span"); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnFinishSpan() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onFinishSpan(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("span.pending.finished", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnActivateScope() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onActivateScope(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("scope.activate.count", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnCloseScope() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onCloseScope(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("scope.close.count", 1L); + verifyNoMoreInteractions(statsD); + } + + @TableTest({"scenario | manual", "automatic | false ", "manual | true "}) + @ParameterizedTest(name = "testOnScopeCloseError [{index}]") + void testOnScopeCloseError(boolean manual) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(manual ? 2 : 1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onScopeCloseError(manual); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("scope.close.error", 1L); + if (manual) { + verify(statsD).count("scope.user.close.error", 1L); + } + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnScopeStackOverflow() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onScopeStackOverflow(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + verify(statsD).count("scope.error.stack-overflow", 1L); + verifyNoMoreInteractions(statsD); + } + + @Test + void testOnLongRunningUpdate() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(3); + try (TracerHealthMetrics healthMetrics = + new TracerHealthMetrics(new Latched(statsD, latch), 100, TimeUnit.MILLISECONDS)) { + healthMetrics.start(); + healthMetrics.onLongRunningUpdate(3, 10, 1); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + verify(statsD).count("long-running.write", 10L); + verify(statsD).count("long-running.dropped", 3L); + verify(statsD).count("long-running.expired", 1L); + verifyNoMoreInteractions(statsD); + } + + private static class Latched implements StatsDClient { + private final StatsDClient delegate; + private final CountDownLatch latch; + + Latched(StatsDClient delegate, CountDownLatch latch) { + this.delegate = delegate; + this.latch = latch; + } + + @Override + public void incrementCounter(String metricName, String... tags) { + try { + delegate.incrementCounter(metricName, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void count(String metricName, long delta, String... tags) { + try { + delegate.count(metricName, delta, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void gauge(String metricName, long value, String... tags) { + try { + delegate.gauge(metricName, value, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void gauge(String metricName, double value, String... tags) { + try { + delegate.gauge(metricName, value, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void histogram(String metricName, long value, String... tags) { + try { + delegate.histogram(metricName, value, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void histogram(String metricName, double value, String... tags) { + try { + delegate.histogram(metricName, value, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void distribution(String metricName, long value, String... tags) { + try { + delegate.distribution(metricName, value, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void distribution(String metricName, double value, String... tags) { + try { + delegate.distribution(metricName, value, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void serviceCheck( + String serviceCheckName, String status, String message, String... tags) { + try { + delegate.serviceCheck(serviceCheckName, status, message, tags); + } finally { + latch.countDown(); + } + } + + @Override + public void error(Exception error) { + try { + delegate.error(error); + } finally { + latch.countDown(); + } + } + + @Override + public int getErrorCount() { + try { + return delegate.getErrorCount(); + } finally { + latch.countDown(); + } + } + + @Override + public void close() { + try { + delegate.close(); + } finally { + latch.countDown(); + } + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java new file mode 100644 index 00000000000..e12c1a0dfa6 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/tagprocessor/HttpEndpointPostProcessorTest.java @@ -0,0 +1,104 @@ +package datadog.trace.core.tagprocessor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyByte; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import datadog.trace.api.TagMap; +import datadog.trace.api.endpoint.EndpointResolver; +import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.core.DDSpanContext; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HttpEndpointPostProcessorTest { + + @Mock DDSpanContext mockContext; + @Mock AppendableSpanLinks mockSpanLinks; + + @Test + void shouldNotOverwriteResourceNameWhenHttpRouteIsAvailableAndEligible() { + EndpointResolver endpointResolver = new EndpointResolver(true, false); + HttpEndpointPostProcessor processor = new HttpEndpointPostProcessor(endpointResolver); + Map tagInput = new HashMap<>(); + tagInput.put(Tags.HTTP_METHOD, "GET"); + tagInput.put(Tags.HTTP_ROUTE, "/greeting"); + tagInput.put(Tags.HTTP_URL, "http://localhost:8080/greeting"); + TagMap tags = TagMap.fromMap(tagInput); + + processor.processTags(tags, mockContext, mockSpanLinks); + + verify(mockContext, never()).setResourceName(any(CharSequence.class), anyByte()); + assertFalse(tags.containsKey(Tags.HTTP_ENDPOINT)); + } + + @Test + void shouldComputeAndTagHttpEndpointFromUrlWhenRouteIsInvalidWithoutTouchingResourceName() { + EndpointResolver endpointResolver = new EndpointResolver(true, false); + HttpEndpointPostProcessor processor = new HttpEndpointPostProcessor(endpointResolver); + Map tagInput = new HashMap<>(); + tagInput.put(Tags.HTTP_METHOD, "GET"); + tagInput.put(Tags.HTTP_ROUTE, "*"); // catch-all — ineligible per RFC-1051 + tagInput.put(Tags.HTTP_URL, "http://localhost:8080/users/123/orders/456"); + TagMap tags = TagMap.fromMap(tagInput); + + processor.processTags(tags, mockContext, mockSpanLinks); + + verify(mockContext, never()).setResourceName(any(CharSequence.class), anyByte()); + assertEquals("/users/{param:int}/orders/{param:int}", tags.get(Tags.HTTP_ENDPOINT)); + } + + @Test + void shouldSkipNonHttpSpans() { + EndpointResolver endpointResolver = new EndpointResolver(true, false); + HttpEndpointPostProcessor processor = new HttpEndpointPostProcessor(endpointResolver); + Map tagInput = new HashMap<>(); + tagInput.put("db.statement", "SELECT * FROM users"); + TagMap tags = TagMap.fromMap(tagInput); + + processor.processTags(tags, mockContext, mockSpanLinks); + + verify(mockContext, never()).setResourceName(any(CharSequence.class), anyByte()); + assertFalse(tags.containsKey(Tags.HTTP_ENDPOINT)); + } + + @Test + void shouldNotProcessWhenResourceRenamingIsDisabled() { + EndpointResolver endpointResolver = new EndpointResolver(false, false); + HttpEndpointPostProcessor processor = new HttpEndpointPostProcessor(endpointResolver); + Map tagInput = new HashMap<>(); + tagInput.put(Tags.HTTP_METHOD, "GET"); + tagInput.put(Tags.HTTP_ROUTE, "/greeting"); + TagMap tags = TagMap.fromMap(tagInput); + + processor.processTags(tags, mockContext, mockSpanLinks); + + verify(mockContext, never()).setResourceName(any(CharSequence.class), anyByte()); + assertFalse(tags.containsKey(Tags.HTTP_ENDPOINT)); + } + + @Test + void shouldTagHttpEndpointFromUrlEvenWhenAlwaysSimplifiedIsTrueWithoutTouchingResourceName() { + EndpointResolver endpointResolver = new EndpointResolver(true, true); + HttpEndpointPostProcessor processor = new HttpEndpointPostProcessor(endpointResolver); + Map tagInput = new HashMap<>(); + tagInput.put(Tags.HTTP_METHOD, "GET"); + tagInput.put(Tags.HTTP_ROUTE, "/greeting"); + tagInput.put(Tags.HTTP_URL, "http://localhost:8080/users/123"); + TagMap tags = TagMap.fromMap(tagInput); + + processor.processTags(tags, mockContext, mockSpanLinks); + + verify(mockContext, never()).setResourceName(any(CharSequence.class), anyByte()); + assertEquals("/users/{param:int}", tags.get(Tags.HTTP_ENDPOINT)); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java new file mode 100644 index 00000000000..05e14e33f52 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java @@ -0,0 +1,203 @@ +package datadog.trace.core.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class StackTracesTest { + + private static final String TRACE = + "\n" + + "Exception in thread \"main\" com.example.app.MainException: Unexpected application failure\n" + + " at com.example.app.Application$Runner.run(Application.java:102)\n" + + " at com.example.app.Application.lambda$start$0(Application.java:75)\n" + + " at java.base/java.util.Optional.ifPresent(Optional.java:178)\n" + + " at com.example.app.Application.start(Application.java:74)\n" + + " at com.example.app.Main.main(Main.java:21)\n" + + " at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n" + + " at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n" + + " at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n" + + " at java.base/java.lang.reflect.Method.invoke(Method.java:566)\n" + + " at com.example.launcher.Bootstrap.run(Bootstrap.java:39)\n" + + " at com.example.launcher.Bootstrap.main(Bootstrap.java:25)\n" + + " at com.example.internal.$Proxy1.start(Unknown Source)\n" + + " at com.example.internal.Initializer$1.run(Initializer.java:47)\n" + + " at com.example.internal.Initializer.lambda$init$0(Initializer.java:38)\n" + + " at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)\n" + + " at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)\n" + + " at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n" + + " at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n" + + " at java.base/java.lang.Thread.run(Thread.java:834)\n" + + " at com.example.synthetic.Helper.access$100(Helper.java:14)\n" + + "Caused by: com.example.db.DatabaseException: Failed to load user data\n" + + " at com.example.db.UserDao.findUser(UserDao.java:88)\n" + + " at com.example.db.UserDao.lambda$cacheLookup$1(UserDao.java:64)\n" + + " at com.example.cache.Cache$Entry.computeIfAbsent(Cache.java:111)\n" + + " at com.example.cache.Cache.get(Cache.java:65)\n" + + " at com.example.service.UserService.loadUser(UserService.java:42)\n" + + " at com.example.service.UserService.lambda$loadUserAsync$0(UserService.java:36)\n" + + " at com.example.util.SafeRunner.run(SafeRunner.java:27)\n" + + " at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)\n" + + " at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)\n" + + " at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n" + + " at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n" + + " at java.base/java.lang.Thread.run(Thread.java:834)\n" + + " at com.example.synthetic.UserDao$1.run(UserDao.java:94)\n" + + " at com.example.synthetic.UserDao$1.run(UserDao.java:94)\n" + + " at com.example.db.ConnectionManager.getConnection(ConnectionManager.java:55)\n" + + "Suppressed: java.io.IOException: Resource cleanup failed\n" + + " at com.example.util.ResourceManager.close(ResourceManager.java:23)\n" + + " at com.example.service.UserService.lambda$loadUserAsync$0(UserService.java:38)\n" + + " ... 3 more\n" + + "Caused by: java.nio.file.AccessDeniedException: /data/user/config.json\n" + + " at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90)\n" + + " at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)\n" + + " at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116)\n" + + " at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219)\n" + + " at java.base/java.nio.file.Files.newByteChannel(Files.java:375)\n" + + " at java.base/java.nio.file.Files.newInputStream(Files.java:489)\n" + + " at com.example.util.FileUtils.readFile(FileUtils.java:22)\n" + + " at com.example.util.ResourceManager.close(ResourceManager.java:21)\n" + + " ... 3 more\n"; + + @ParameterizedTest(name = "truncation limit {0}") + @MethodSource("testTruncateArguments") + void testTruncate(int limit, String expected) { + assertEquals(expected, StackTraces.truncate(TRACE, limit)); + } + + static Stream testTruncateArguments() { + return Stream.of( + arguments(1000, expected1000()), + arguments(2500, expected2500()), + arguments(3000, expected3000())); + } + + private static String expected1000() { + return "\n" + + "Exception in thread \"main\" com.example.app.MainException: Unexpected application failure\n" + + "\tat c.e.a.Application$Runner.run(Application.java:102)\n" + + "\tat c.e.a.Application.lambda$start$0(Application.java:75)\n" + + "\tat j.b.u.Optional.ifPresent(Optional.java:178)\n" + + "\tat c.e.a.Application.start(Application.java:74)\n" + + "\tat c.e.a.Main.main(Main.java:21)\n" + + "\tat s.r.NativeMethodAccessorImpl.invoke0(Native Method)\n" + + "\tat s.r.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n" + + "\tat s.r.Delegat\n" + + "\t... trace centre-cut to 1000 chars ...\n" + + "ToIOException(UnixException.java:90)\n" + + "\tat j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:111)\n" + + "\tat j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:116)\n" + + "\tat j.b.n.f.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219)\n" + + "\tat j.b.n.f.Files.newByteChannel(Files.java:375)\n" + + "\tat j.b.n.f.Files.newInputStream(Files.java:489)\n" + + "\tat c.e.u.FileUtils.readFile(FileUtils.java:22)\n" + + "\tat c.e.u.ResourceManager.close(ResourceManager.java:21)\n" + + " ... 3 more\n"; + } + + private static String expected2500() { + return "\n" + + "Exception in thread \"main\" com.example.app.MainException: Unexpected application failure\n" + + "\tat c.e.a.Application$Runner.run(Application.java:102)\n" + + "\tat c.e.a.Application.lambda$start$0(Application.java:75)\n" + + "\tat j.b.u.Optional.ifPresent(Optional.java:178)\n" + + "\tat c.e.a.Application.start(Application.java:74)\n" + + "\tat c.e.a.Main.main(Main.java:21)\n" + + "\tat s.r.NativeMethodAccessorImpl.invoke0(Native Method)\n" + + "\tat s.r.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n" + + "\tat s.r.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n" + + "\t... 8 trimmed ...\n" + + "\tat j.b.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n" + + "\tat j.b.u.c.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n" + + "\tat j.b.l.Thread.run(Thread.java:834)\n" + + "\tat c.e.s.Helper.access$100(Helper.java:14)\n" + + "Caused by: com.example.db.DatabaseException: Failed to load user data\n" + + "\tat c.e.d.UserDao.findUser(UserDao.java:88)\n" + + "\tat c.e.d.UserDao.lambda$cacheLookup$1(UserDao.java:64)\n" + + "\tat c.e.c.Cache$Entry.computeIfAbsent(Cache.java:111)\n" + + "\tat c.e.c.Cache.get(Cache.java:65)\n" + + "\tat c.e.s.UserService.loadUser(UserService.java:42)\n" + + "\tat c.e.s.UserService.lambda$loadUserAsync$0(UserService.java:36)\n" + + "\tat c.e.u.SafeRunner.run(SafeRunner.java:27)\n" + + "\tat j.b.u.c.Executors$RunnableAdapter.call(Executors.java:515)\n" + + "\t... 3 trimmed ...\n" + + "\tat j.b.l.Thread.run(Thread.java:834)\n" + + "\tat c.e.s.UserDao$1.run(UserDao.java:94)\n" + + "\tat c.e.s.UserDao$1.run(UserDao.java:94)\n" + + "\tat c.e.d.ConnectionManager.getConnection(ConnectionManager.java:55)\n" + + "Suppressed: java.io.IOException: Resource cleanup failed\n" + + "\tat c.e.u.ResourceManager.close(ResourceManager.java:23)\n" + + "\tat c.e.s.UserService.lambda$loadUserAsync$0(UserService.java:38)\n" + + " ... 3 more\n" + + "Caused by: java.nio.file.AccessDeniedException: /data/user/config.json\n" + + "\tat j.b.n.f.UnixException.translateToIOException(UnixException.java:90)\n" + + "\tat j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:111)\n" + + "\tat j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:116)\n" + + "\tat j.b.n.f.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219)\n" + + "\tat j.b.n.f.Files.newByteChannel(Files.java:375)\n" + + "\tat j.b.n.f.Files.newInputStream(Files.java:489)\n" + + "\tat c.e.u.FileUtils.readFile(FileUtils.java:22)\n" + + "\tat c.e.u.ResourceManager.close(ResourceManager.java:21)\n" + + " ... 3 more\n"; + } + + private static String expected3000() { + return "\n" + + "Exception in thread \"main\" com.example.app.MainException: Unexpected application failure\n" + + "\tat c.e.a.Application$Runner.run(Application.java:102)\n" + + "\tat c.e.a.Application.lambda$start$0(Application.java:75)\n" + + "\tat j.b.u.Optional.ifPresent(Optional.java:178)\n" + + "\tat c.e.a.Application.start(Application.java:74)\n" + + "\tat c.e.a.Main.main(Main.java:21)\n" + + "\tat s.r.NativeMethodAccessorImpl.invoke0(Native Method)\n" + + "\tat s.r.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n" + + "\tat s.r.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n" + + "\tat j.b.l.r.Method.invoke(Method.java:566)\n" + + "\tat c.e.l.Bootstrap.run(Bootstrap.java:39)\n" + + "\tat c.e.l.Bootstrap.main(Bootstrap.java:25)\n" + + "\tat c.e.i.$Proxy1.start(Unknown Source)\n" + + "\tat c.e.i.Initializer$1.run(Initializer.java:47)\n" + + "\tat c.e.i.Initializer.lambda$init$0(Initializer.java:38)\n" + + "\tat j.b.u.c.Executors$RunnableAdapter.call(Executors.java:515)\n" + + "\tat j.b.u.c.FutureTask.run(FutureTask.java:264)\n" + + "\tat j.b.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n" + + "\tat j.b.u.c.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n" + + "\tat j.b.l.Thread.run(Thread.java:834)\n" + + "\tat c.e.s.Helper.access$100(Helper.java:14)\n" + + "Caused by: com.example.db.DatabaseException: Failed to load user data\n" + + "\tat c.e.d.UserDao.findUser(UserDao.java:88)\n" + + "\tat c.e.d.UserDao.lambda$cacheLookup$1(UserDao.java:64)\n" + + "\tat c.e.c.Cache$Entry.computeIfAbsent(Cache.java:111)\n" + + "\tat c.e.c.Cache.get(Cache.java:65)\n" + + "\tat c.e.s.UserService.loadUser(UserService.java:42)\n" + + "\tat c.e.s.UserService.lambda$loadUserAsync$0(UserService.java:36)\n" + + "\tat c.e.u.SafeRunner.run(SafeRunner.java:27)\n" + + "\tat j.b.u.c.Executors$RunnableAdapter.call(Executors.java:515)\n" + + "\tat j.b.u.c.FutureTask.run(FutureTask.java:264)\n" + + "\tat j.b.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n" + + "\tat j.b.u.c.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n" + + "\tat j.b.l.Thread.run(Thread.java:834)\n" + + "\tat c.e.s.UserDao$1.run(UserDao.java:94)\n" + + "\tat c.e.s.UserDao$1.run(UserDao.java:94)\n" + + "\tat c.e.d.ConnectionManager.getConnection(ConnectionManager.java:55)\n" + + "Suppressed: java.io.IOException: Resource cleanup failed\n" + + "\tat c.e.u.ResourceManager.close(ResourceManager.java:23)\n" + + "\tat c.e.s.UserService.lambda$loadUserAsync$0(UserService.java:38)\n" + + " ... 3 more\n" + + "Caused by: java.nio.file.AccessDeniedException: /data/user/config.json\n" + + "\tat j.b.n.f.UnixException.translateToIOException(UnixException.java:90)\n" + + "\tat j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:111)\n" + + "\tat j.b.n.f.UnixException.rethrowAsIOException(UnixException.java:116)\n" + + "\tat j.b.n.f.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219)\n" + + "\tat j.b.n.f.Files.newByteChannel(Files.java:375)\n" + + "\tat j.b.n.f.Files.newInputStream(Files.java:489)\n" + + "\tat c.e.u.FileUtils.readFile(FileUtils.java:22)\n" + + "\tat c.e.u.ResourceManager.close(ResourceManager.java:21)\n" + + " ... 3 more\n"; + } +}