diff --git a/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioClientDecorator.java b/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioClientDecorator.java index 7fdea18f81..7d8a75b69c 100644 --- a/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioClientDecorator.java +++ b/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioClientDecorator.java @@ -37,7 +37,7 @@ public class TwilioClientDecorator extends ClientDecorator { return COMPONENT_NAME; } - /** Decorate trace based on service execution metadata */ + /** Decorate trace based on service execution metadata. */ public Span onServiceExecution( final Span span, final Object serviceExecutor, final String methodName) { @@ -76,13 +76,17 @@ public class TwilioClientDecorator extends ClientDecorator { span.setTag("twilio.type", result.getClass().getCanonicalName()); span.setTag("twilio.account", message.getAccountSid()); span.setTag("twilio.sid", message.getSid()); - span.setTag("twilio.status", message.getStatus().toString()); + if (message.getStatus() != null) { + span.setTag("twilio.status", message.getStatus().toString()); + } } else if (result instanceof Call) { final Call call = (Call) result; span.setTag("twilio.account", call.getAccountSid()); span.setTag("twilio.sid", call.getSid()); span.setTag("twilio.parentSid", call.getParentCallSid()); - span.setTag("twilio.status", call.getStatus().toString()); + if (call.getStatus() != null) { + span.setTag("twilio.status", call.getStatus().toString()); + } } else { // Use reflection to gather insight from other types; note that Twilio requests take close to // 1 second, so the added hit from reflection here is relatively minimal in the grand scheme diff --git a/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioInstrumentation.java b/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioInstrumentation.java index 0de7fb491d..59bed69105 100644 --- a/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioInstrumentation.java +++ b/dd-java-agent/instrumentation/twilio/src/main/java/datadog/trace/instrumentation/twilio/TwilioInstrumentation.java @@ -64,9 +64,10 @@ public class TwilioInstrumentation extends Instrumenter.Default { public Map, String> transformers() { /* - We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and Updater abstract - classes. The isDeclaredBy() matcher did not work in the unit tests and we found that there were certain - methods declared on the base class (particularly Reader), which we weren't interested in annotating. + We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and + Updater abstract classes. The isDeclaredBy() matcher did not work in the unit tests and + we found that there were certain methods declared on the base class (particularly Reader), + which we weren't interested in annotating. */ return singletonMap( @@ -85,7 +86,7 @@ public class TwilioInstrumentation extends Instrumenter.Default { /** Advice for instrumenting Twilio service classes. */ public static class TwilioClientAdvice { - /** Method entry instrumentation */ + /** Method entry instrumentation. */ @Advice.OnMethodEnter(suppress = Throwable.class) public static Scope methodEnter( @Advice.This final Object that, @Advice.Origin("#m") final String methodName) { @@ -121,7 +122,7 @@ public class TwilioInstrumentation extends Instrumenter.Default { return scope; } - /** Method exit instrumentation */ + /** Method exit instrumentation. */ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( @Advice.Enter final Scope scope, @@ -137,6 +138,7 @@ public class TwilioInstrumentation extends Instrumenter.Default { final Span span = scope.span(); DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); // If we're calling an async operation, we still need to finish the span when it's // complete and report the results; set an appropriate callback @@ -165,7 +167,7 @@ public class TwilioInstrumentation extends Instrumenter.Default { */ public static class SpanFinishingCallback implements FutureCallback { - /** Span that we should finish and annotate when the future is complete */ + /** Span that we should finish and annotate when the future is complete. */ private final Span span; public SpanFinishingCallback(final Span span) { diff --git a/dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy b/dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy index 5e14b0145e..cf3f5b01d6 100644 --- a/dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy +++ b/dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy @@ -4,14 +4,22 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.util.concurrent.ListenableFuture import com.twilio.Twilio import com.twilio.exception.ApiException +import com.twilio.http.NetworkHttpClient import com.twilio.http.Response import com.twilio.http.TwilioRestClient +import com.twilio.rest.api.v2010.account.Call import com.twilio.rest.api.v2010.account.Message import com.twilio.type.PhoneNumber import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.DDSpanTypes import io.opentracing.tag.Tags import io.opentracing.util.GlobalTracer +import org.apache.http.HttpEntity +import org.apache.http.HttpStatus +import org.apache.http.StatusLine +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClientBuilder import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit @@ -46,6 +54,41 @@ class TwilioClientTest extends AgentTestRunner { } """ + final static String CALL_RESPONSE_BODY = """ + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "annotation": null, + "answered_by": null, + "api_version": "2010-04-01", + "caller_name": null, + "date_created": "Tue, 31 Aug 2010 20:36:28 +0000", + "date_updated": "Tue, 31 Aug 2010 20:36:44 +0000", + "direction": "inbound", + "duration": "15", + "end_time": "Tue, 31 Aug 2010 20:36:44 +0000", + "forwarded_from": "+141586753093", + "from": "+15017122661", + "from_formatted": "(501) 712-2661", + "group_sid": null, + "parent_call_sid": null, + "phone_number_sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "price": -0.03000, + "price_unit": "USD", + "sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "start_time": "Tue, 31 Aug 2010 20:36:29 +0000", + "status": "completed", + "subresource_uris": { + "notifications": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json", + "recordings": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json", + "feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json", + "feedback_summaries": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json" + }, + "to": "+15558675310", + "to_formatted": "(555) 867-5310", + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" + } + """ + final static String ERROR_RESPONSE_BODY = """ { "code": 123, @@ -61,7 +104,7 @@ class TwilioClientTest extends AgentTestRunner { Twilio.init(ACCOUNT_SID, AUTH_TOKEN) } - def "synchronous call"() { + def "synchronous message"() { setup: twilioRestClient.getObjectMapper() >> new ObjectMapper() @@ -75,8 +118,6 @@ class TwilioClientTest extends AgentTestRunner { "Hello world!" // SMS body ).create(twilioRestClient) - Thread.sleep(1000); - def scope = GlobalTracer.get().scopeManager().active() if (scope) { scope.close() @@ -115,6 +156,384 @@ class TwilioClientTest extends AgentTestRunner { } } + def "synchronous call"() { + setup: + twilioRestClient.getObjectMapper() >> new ObjectMapper() + + 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(CALL_RESPONSE_BODY.getBytes()), 200) + + GlobalTracer.get().buildSpan("test").startActive(true) + + Call call = Call.creator( + new PhoneNumber("+15558881234"), // To number + new PhoneNumber("+15559994321"), // From number + + // Read TwiML at this URL when a call connects (hold music) + new URI("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient") + ).create(twilioRestClient) + + def scope = GlobalTracer.get().scopeManager().active() + if (scope) { + scope.close() + } + + expect: + + call.status == Call.Status.COMPLETED + + assertTraces(1) { + trace(0, 2) { + span(0) { + serviceName "unnamed-java-app" + operationName "test" + resourceName "test" + errored false + parent() + } + span(1) { + serviceName "twilio-sdk" + operationName "twilio.sdk" + resourceName "api.v2010.account.CallCreator.create" + spanType DDSpanTypes.HTTP_CLIENT + errored false + tags { + "$Tags.COMPONENT.key" "twilio-sdk" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "twilio.type" "com.twilio.rest.api.v2010.account.Call" + "twilio.account" "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.sid" "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "completed" + defaultTags() + } + } + } + } + } + + + def "http client"() { + setup: + HttpClientBuilder clientBuilder = Mock() + CloseableHttpClient httpClient = Mock() + CloseableHttpResponse httpResponse = Mock() + HttpEntity httpEntity = Mock() + StatusLine statusLine = Mock() + + clientBuilder.build() >> httpClient + + httpClient.execute(_) >> httpResponse + + httpResponse.getEntity() >> httpEntity + httpResponse.getStatusLine() >> statusLine + + httpEntity.getContent() >> { new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) } + httpEntity.isRepeatable() >> true + httpEntity.getContentLength() >> MESSAGE_RESPONSE_BODY.length() + + statusLine.getStatusCode() >> HttpStatus.SC_OK + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build() + + GlobalTracer.get().buildSpan("test").startActive(true) + + Message message = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(realTwilioRestClient) + + def scope = GlobalTracer.get().scopeManager().active() + if (scope) { + scope.close() + } + + expect: + + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 3) { + span(0) { + serviceName "unnamed-java-app" + operationName "test" + resourceName "test" + errored false + parent() + } + span(1) { + serviceName "twilio-sdk" + operationName "twilio.sdk" + resourceName "api.v2010.account.MessageCreator.create" + spanType DDSpanTypes.HTTP_CLIENT + errored false + tags { + "$Tags.COMPONENT.key" "twilio-sdk" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + defaultTags() + } + } + span(2) { + serviceName "twilio-sdk" + operationName "http.request" + resourceName "POST /?/Accounts/abc/Messages.json" + spanType DDSpanTypes.HTTP_CLIENT + errored false + } + } + } + } + + def "http client retry"() { + setup: + HttpClientBuilder clientBuilder = Mock() + CloseableHttpClient httpClient = Mock() + CloseableHttpResponse httpResponse1 = Mock() + CloseableHttpResponse httpResponse2 = Mock() + HttpEntity httpEntity1 = Mock() + HttpEntity httpEntity2 = Mock() + StatusLine statusLine1 = Mock() + StatusLine statusLine2 = Mock() + + clientBuilder.build() >> httpClient + + httpClient.execute(_) >>> [httpResponse1, httpResponse2] + + // First response is an HTTP/500 error, which should drive a retry + httpResponse1.getEntity() >> httpEntity1 + httpResponse1.getStatusLine() >> statusLine1 + + httpEntity1.getContent() >> { new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()) } + + httpEntity1.isRepeatable() >> true + httpEntity1.getContentLength() >> ERROR_RESPONSE_BODY.length() + + statusLine1.getStatusCode() >> HttpStatus.SC_INTERNAL_SERVER_ERROR + + // Second response is HTTP/200 success + httpResponse2.getEntity() >> httpEntity2 + httpResponse2.getStatusLine() >> statusLine2 + + httpEntity2.getContent() >> { + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) + } + httpEntity2.isRepeatable() >> true + httpEntity2.getContentLength() >> MESSAGE_RESPONSE_BODY.length() + + statusLine2.getStatusCode() >> HttpStatus.SC_OK + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build() + + GlobalTracer.get().buildSpan("test").startActive(true) + + Message message = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).create(realTwilioRestClient) + + def scope = GlobalTracer.get().scopeManager().active() + if (scope) { + scope.close() + } + + expect: + + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 4) { + span(0) { + serviceName "unnamed-java-app" + operationName "test" + resourceName "test" + errored false + parent() + } + span(1) { + serviceName "twilio-sdk" + operationName "twilio.sdk" + resourceName "api.v2010.account.MessageCreator.create" + spanType DDSpanTypes.HTTP_CLIENT + errored false + tags { + "$Tags.COMPONENT.key" "twilio-sdk" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + defaultTags() + } + } + span(2) { + serviceName "twilio-sdk" + operationName "http.request" + resourceName "POST /?/Accounts/abc/Messages.json" + spanType DDSpanTypes.HTTP_CLIENT + errored false + } + span(3) { + serviceName "twilio-sdk" + operationName "http.request" + resourceName "POST /?/Accounts/abc/Messages.json" + spanType DDSpanTypes.HTTP_CLIENT + errored true + } + } + } + } + + def "http client retry async"() { + setup: + HttpClientBuilder clientBuilder = Mock() + CloseableHttpClient httpClient = Mock() + CloseableHttpResponse httpResponse1 = Mock() + CloseableHttpResponse httpResponse2 = Mock() + HttpEntity httpEntity1 = Mock() + HttpEntity httpEntity2 = Mock() + StatusLine statusLine1 = Mock() + StatusLine statusLine2 = Mock() + + clientBuilder.build() >> httpClient + + httpClient.execute(_) >>> [httpResponse1, httpResponse2] + + // First response is an HTTP/500 error, which should drive a retry + httpResponse1.getEntity() >> httpEntity1 + httpResponse1.getStatusLine() >> statusLine1 + + httpEntity1.getContent() >> { new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()) } + + httpEntity1.isRepeatable() >> true + httpEntity1.getContentLength() >> ERROR_RESPONSE_BODY.length() + + statusLine1.getStatusCode() >> HttpStatus.SC_INTERNAL_SERVER_ERROR + + // Second response is HTTP/200 success + httpResponse2.getEntity() >> httpEntity2 + httpResponse2.getStatusLine() >> statusLine2 + + httpEntity2.getContent() >> { + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()) + } + httpEntity2.isRepeatable() >> true + httpEntity2.getContentLength() >> MESSAGE_RESPONSE_BODY.length() + + statusLine2.getStatusCode() >> HttpStatus.SC_OK + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build() + + GlobalTracer.get().buildSpan("test").startActive(true) + + ListenableFuture future = Message.creator( + new PhoneNumber("+1 555 720 5913"), // To number + new PhoneNumber("+1 555 555 5215"), // From number + "Hello world!" // SMS body + ).createAsync(realTwilioRestClient) + + Message message + try { + message = future.get(10, TimeUnit.SECONDS) + } finally { + // Give the future callback a chance to run + Thread.sleep(1000) + def scope = GlobalTracer.get().scopeManager().active() + if (scope) { + scope.span().finish() + scope.close() + } + } + + expect: + + message.body == "Hello, World!" + + assertTraces(1) { + trace(0, 5) { + span(0) { + serviceName "unnamed-java-app" + operationName "test" + resourceName "test" + errored false + parent() + } + span(1) { + serviceName "twilio-sdk" + operationName "twilio.sdk" + resourceName "api.v2010.account.MessageCreator.createAsync" + spanType DDSpanTypes.HTTP_CLIENT + errored false + tags { + "$Tags.COMPONENT.key" "twilio-sdk" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + defaultTags() + } + } + span(2) { + serviceName "twilio-sdk" + operationName "twilio.sdk" + resourceName "api.v2010.account.MessageCreator.create" + spanType DDSpanTypes.HTTP_CLIENT + errored false + tags { + "$Tags.COMPONENT.key" "twilio-sdk" + "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT + "twilio.type" "com.twilio.rest.api.v2010.account.Message" + "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" + "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "twilio.status" "sent" + defaultTags() + } + } + span(3) { + serviceName "twilio-sdk" + operationName "http.request" + resourceName "POST /?/Accounts/abc/Messages.json" + spanType DDSpanTypes.HTTP_CLIENT + errored true + } + span(4) { + serviceName "twilio-sdk" + operationName "http.request" + resourceName "POST /?/Accounts/abc/Messages.json" + spanType DDSpanTypes.HTTP_CLIENT + errored false + } + } + } + + cleanup: + Twilio.getExecutorService().shutdown() + Twilio.setExecutorService(null) + Twilio.setRestClient(null) + } + def "Sync Failure"() { setup: @@ -198,14 +617,17 @@ class TwilioClientTest extends AgentTestRunner { } } } + } - def "asynchronous call"() { + def "asynchronous call"(a) { setup: twilioRestClient.getObjectMapper() >> new ObjectMapper() 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) + when: + GlobalTracer.get().buildSpan("test").startActive(true) ListenableFuture future = Message.creator( @@ -214,14 +636,20 @@ class TwilioClientTest extends AgentTestRunner { "Hello world!" // SMS body ).createAsync(twilioRestClient) - Message message = future.get(10, TimeUnit.SECONDS) - - def scope = GlobalTracer.get().scopeManager().active() - if (scope) { - scope.close() + Message message + try { + message = future.get(10, TimeUnit.SECONDS) + } finally { + // Give the future callback a chance to run + Thread.sleep(1000) + def scope = GlobalTracer.get().scopeManager().active() + if (scope) { + scope.span().finish() + scope.close() + } } - expect: + then: message != null message.body == "Hello, World!" @@ -269,6 +697,16 @@ class TwilioClientTest extends AgentTestRunner { } } } + + cleanup: + Twilio.getExecutorService().shutdown() + Twilio.setExecutorService(null) + Twilio.setRestClient(null) + + where: + a | _ + 1 | _ + 2 | _ } def "asynchronous error"() { @@ -292,6 +730,7 @@ class TwilioClientTest extends AgentTestRunner { message = future.get(10, TimeUnit.SECONDS) } finally { + Thread.sleep(1000) def scope = GlobalTracer.get().scopeManager().active() if (scope) { scope.close() @@ -328,6 +767,11 @@ class TwilioClientTest extends AgentTestRunner { } } } + + cleanup: + Twilio.getExecutorService().shutdown() + Twilio.setExecutorService(null) + Twilio.setRestClient(null) } } diff --git a/dd-java-agent/instrumentation/twilio/twilio.gradle b/dd-java-agent/instrumentation/twilio/twilio.gradle index 48ed0329ac..f8dc7ac327 100644 --- a/dd-java-agent/instrumentation/twilio/twilio.gradle +++ b/dd-java-agent/instrumentation/twilio/twilio.gradle @@ -20,6 +20,9 @@ dependencies { testCompile group: 'com.twilio.sdk', name: 'twilio', version: '7.36.2' testCompile project(':dd-java-agent:testing') + testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4') testCompile project(':dd-java-agent:instrumentation:java-concurrent') - testCompile group: 'org.objenesis', name: 'objenesis', version: '3.0.1' + testCompile group: 'org.objenesis', name: 'objenesis', version: '2.6' // Last version to support Java7 + testCompile group: 'nl.jqno.equalsverifier', name: 'equalsverifier', version: '2.5.2' // Last version to support Java7 + } diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java index 254e3765bb..a02a3a9024 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.java @@ -19,6 +19,7 @@ import groovy.transform.stc.SimpleType; import io.opentracing.Tracer; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; +import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; import java.util.Set; @@ -84,6 +85,8 @@ public abstract class AgentTestRunner extends Specification { TEST_WRITER = new ListWriter() { + private static final long serialVersionUID = 705972961882897201L; + @Override public boolean add(final List trace) { final boolean result = super.add(trace); @@ -194,6 +197,14 @@ public abstract class AgentTestRunner extends Specification { options = "datadog.trace.agent.test.asserts.ListWriterAssert") @DelegatesTo(value = ListWriterAssert.class, strategy = Closure.DELEGATE_FIRST) final Closure spec) { + final Iterator> iterator = TEST_WRITER.iterator(); + while (iterator.hasNext()) { + final List next = iterator.next(); + final Iterator iterator1 = next.iterator(); + while (iterator1.hasNext()) { + System.out.println(iterator1.next()); + } + } ListWriterAssert.assertTraces(TEST_WRITER, size, spec); } @@ -265,6 +276,8 @@ public abstract class AgentTestRunner extends Specification { /** Used to signal that a transformation was intentionally aborted and is not an error. */ public static class AbortTransformationException extends RuntimeException { + private static final long serialVersionUID = -1849465286193994582L; + public AbortTransformationException() { super(); }