From 4d0d0f6e42cfb4b5be59685d8730e96dccdad2d9 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 19 May 2026 11:30:20 +0200 Subject: [PATCH 1/4] Have a fully JMS 1.0 compatible instrumentation --- .../instrumentation/jms/JMSDecorator.java | 15 ++ .../JMSMessageProducerInstrumentation.java | 4 +- .../jms/SessionInstrumentation.java | 4 +- .../src/test/groovy/JMS1Test.groovy | 33 ++- .../test/java/jms10mock/Jms10Connection.java | 130 ++++++++++ .../jms10mock/Jms10ConnectionFactory.java | 61 +++++ .../java/jms10mock/Jms10QueueReceiver.java | 59 +++++ .../test/java/jms10mock/Jms10QueueSender.java | 124 ++++++++++ .../src/test/java/jms10mock/Jms10Session.java | 229 ++++++++++++++++++ .../java/jms10mock/Jms10TopicPublisher.java | 137 +++++++++++ .../java/jms10mock/Jms10TopicSubscriber.java | 66 +++++ 11 files changed, 847 insertions(+), 15 deletions(-) create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Connection.java create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10ConnectionFactory.java create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueReceiver.java create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueSender.java create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Session.java create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicPublisher.java create mode 100644 dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicSubscriber.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSDecorator.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSDecorator.java index 87fbfc55fc7..c9dd753b3da 100644 --- a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSDecorator.java +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSDecorator.java @@ -19,10 +19,13 @@ import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; +import javax.jms.MessageProducer; import javax.jms.Queue; +import javax.jms.QueueSender; import javax.jms.TemporaryQueue; import javax.jms.TemporaryTopic; import javax.jms.Topic; +import javax.jms.TopicPublisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -265,6 +268,18 @@ public CharSequence toResourceName(String destinationName, boolean isQueue) { return joiner.apply(destinationName); } + public Destination getDestination(final MessageProducer messageProducer) throws JMSException { + try { + return messageProducer.getDestination(); // >= 1.1 + } catch (AbstractMethodError ignored) { + // <=1.1 getDestination is not available so we need to pay an additional instanceOf + if (messageProducer instanceof QueueSender) { + return ((QueueSender) messageProducer).getQueue(); + } + return ((TopicPublisher) messageProducer).getTopic(); + } + } + public String getDestinationName(Destination destination) { String name = null; try { diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSMessageProducerInstrumentation.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSMessageProducerInstrumentation.java index 972b4382209..3dbfa0579f5 100644 --- a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSMessageProducerInstrumentation.java +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/JMSMessageProducerInstrumentation.java @@ -90,10 +90,10 @@ public static AgentScope beforeSend( // fall-back when producer wasn't created via standard Session.createProducer API if (null != producerState) { resourceName = producerState.getResourceName(); - Destination destination = producer.getDestination(); + Destination destination = PRODUCER_DECORATE.getDestination(producer); destinationName = PRODUCER_DECORATE.getDestinationName(destination); } else { - Destination destination = producer.getDestination(); + Destination destination = PRODUCER_DECORATE.getDestination(producer); destinationName = PRODUCER_DECORATE.getDestinationName(destination); boolean isQueue = PRODUCER_DECORATE.isQueue(destination); resourceName = PRODUCER_DECORATE.toResourceName(destinationName, isQueue); diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/SessionInstrumentation.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/SessionInstrumentation.java index 3f7b095b3b7..8c3ffa48231 100644 --- a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/SessionInstrumentation.java +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/main/java/datadog/trace/instrumentation/jms/SessionInstrumentation.java @@ -114,7 +114,7 @@ public static void bindProducerState( int ackMode; try { ackMode = session.getAcknowledgeMode(); - } catch (Exception ignored) { + } catch (Throwable ignored) { ackMode = Session.AUTO_ACKNOWLEDGE; } sessionState = @@ -155,7 +155,7 @@ public static void bindConsumerState( int ackMode; try { ackMode = session.getAcknowledgeMode(); - } catch (Exception ignored) { + } catch (Throwable ignored) { ackMode = Session.AUTO_ACKNOWLEDGE; } sessionState = diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/groovy/JMS1Test.groovy b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/groovy/JMS1Test.groovy index f059016bc62..f24b7e8f9ff 100644 --- a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/groovy/JMS1Test.groovy +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/groovy/JMS1Test.groovy @@ -7,32 +7,32 @@ import datadog.trace.agent.test.naming.VersionedNamingTestBase import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.Trace -import datadog.trace.api.config.TracerConfig import datadog.trace.api.config.TraceInstrumentationConfig +import datadog.trace.api.config.TracerConfig import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.core.DDSpan -import org.apache.activemq.ActiveMQConnectionFactory -import org.apache.activemq.command.ActiveMQTextMessage -import org.apache.activemq.junit.EmbeddedActiveMQBroker -import spock.lang.Shared - +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference import javax.jms.Connection +import javax.jms.ConnectionFactory import javax.jms.Destination import javax.jms.Message import javax.jms.MessageListener +import javax.jms.Queue import javax.jms.QueueConnection import javax.jms.QueueSession import javax.jms.Session import javax.jms.TemporaryQueue import javax.jms.TemporaryTopic -import javax.jms.Queue -import javax.jms.Topic import javax.jms.TextMessage +import javax.jms.Topic import javax.jms.TopicConnection import javax.jms.TopicSession -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicReference +import jms10mock.Jms10ConnectionFactory +import org.apache.activemq.command.ActiveMQTextMessage +import org.apache.activemq.junit.EmbeddedActiveMQBroker +import spock.lang.Shared abstract class JMS1Test extends VersionedNamingTestBase { @Shared @@ -69,9 +69,13 @@ abstract class JMS1Test extends VersionedNamingTestBase { true } + def createConnectionFactory() { + broker.createConnectionFactory() + } + def setupSpec() { broker.start() - final ActiveMQConnectionFactory connectionFactory = broker.createConnectionFactory() + final ConnectionFactory connectionFactory = createConnectionFactory() connection = connectionFactory.createConnection() connection.start() @@ -1097,3 +1101,10 @@ class JMS1V1ForkedTest extends JMS1Test { "jms.process" } } + +class JMS10Test extends JMS1V0Test { + @Override + def createConnectionFactory() { + new Jms10ConnectionFactory(super.createConnectionFactory()) + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Connection.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Connection.java new file mode 100644 index 00000000000..0f8721a2b89 --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Connection.java @@ -0,0 +1,130 @@ +package jms10mock; + +import javax.jms.Connection; +import javax.jms.ConnectionConsumer; +import javax.jms.ConnectionMetaData; +import javax.jms.Destination; +import javax.jms.ExceptionListener; +import javax.jms.JMSException; +import javax.jms.Queue; +import javax.jms.QueueConnection; +import javax.jms.QueueSession; +import javax.jms.ServerSessionPool; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicSession; + +/** Wraps a real {@link Connection} but simulates a JMS 1.0 provider. */ +public class Jms10Connection implements QueueConnection, TopicConnection { + private final Connection delegate; + + public Jms10Connection(Connection delegate) { + this.delegate = delegate; + } + + // --- JMS 1.1-only unified Connection method --- + + @Override + public Session createSession(boolean transacted, int acknowledgeMode) throws JMSException { + throw new AbstractMethodError( + "JMS 1.0 provider does not implement createSession(boolean, int) on Connection"); + } + + // --- JMS 1.0 QueueConnection methods --- + + @Override + public QueueSession createQueueSession(boolean transacted, int acknowledgeMode) + throws JMSException { + return new Jms10Session(delegate.createSession(transacted, acknowledgeMode)); + } + + // --- JMS 1.0 TopicConnection methods --- + + @Override + public TopicSession createTopicSession(boolean transacted, int acknowledgeMode) + throws JMSException { + return new Jms10Session(delegate.createSession(transacted, acknowledgeMode)); + } + + // --- Common Connection methods --- + + @Override + public String getClientID() throws JMSException { + return delegate.getClientID(); + } + + @Override + public void setClientID(String clientID) throws JMSException { + delegate.setClientID(clientID); + } + + @Override + public ConnectionMetaData getMetaData() throws JMSException { + return delegate.getMetaData(); + } + + @Override + public ExceptionListener getExceptionListener() throws JMSException { + return delegate.getExceptionListener(); + } + + @Override + public void setExceptionListener(ExceptionListener listener) throws JMSException { + delegate.setExceptionListener(listener); + } + + @Override + public void start() throws JMSException { + delegate.start(); + } + + @Override + public void stop() throws JMSException { + delegate.stop(); + } + + @Override + public void close() throws JMSException { + delegate.close(); + } + + // --- ConnectionConsumer methods — not commonly used, throw for JMS 1.1 unified form --- + + @Override + public ConnectionConsumer createConnectionConsumer( + Destination destination, + String messageSelector, + ServerSessionPool sessionPool, + int maxMessages) + throws JMSException { + throw new AbstractMethodError( + "JMS 1.0 provider does not implement createConnectionConsumer(Destination, ...)"); + } + + @Override + public ConnectionConsumer createConnectionConsumer( + Queue queue, String messageSelector, ServerSessionPool sessionPool, int maxMessages) + throws JMSException { + return delegate.createConnectionConsumer(queue, messageSelector, sessionPool, maxMessages); + } + + @Override + public ConnectionConsumer createConnectionConsumer( + Topic topic, String messageSelector, ServerSessionPool sessionPool, int maxMessages) + throws JMSException { + return delegate.createConnectionConsumer(topic, messageSelector, sessionPool, maxMessages); + } + + @Override + public ConnectionConsumer createDurableConnectionConsumer( + Topic topic, + String subscriptionName, + String messageSelector, + ServerSessionPool sessionPool, + int maxMessages) + throws JMSException { + return delegate.createDurableConnectionConsumer( + topic, subscriptionName, messageSelector, sessionPool, maxMessages); + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10ConnectionFactory.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10ConnectionFactory.java new file mode 100644 index 00000000000..1660765f731 --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10ConnectionFactory.java @@ -0,0 +1,61 @@ +package jms10mock; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; + +/** + * Wraps a real {@link ConnectionFactory} but simulates a JMS 1.0 provider. + * + *

In JMS 1.0, clients used the domain-specific {@link QueueConnectionFactory} and {@link + * TopicConnectionFactory} to obtain connections. The unified {@link ConnectionFactory} and its + * {@code createConnection()} methods are JMS 1.1 additions that this wrapper does not support. + */ +public class Jms10ConnectionFactory implements QueueConnectionFactory, TopicConnectionFactory { + private final ConnectionFactory delegate; + + public Jms10ConnectionFactory(ConnectionFactory delegate) { + this.delegate = delegate; + } + + // --- JMS 1.1-only unified ConnectionFactory methods --- + + @Override + public Connection createConnection() throws JMSException { + return delegate.createConnection(); + } + + @Override + public Connection createConnection(String userName, String password) throws JMSException { + return delegate.createConnection(userName, password); + } + + // --- JMS 1.0 QueueConnectionFactory methods --- + @Override + public QueueConnection createQueueConnection() throws JMSException { + return new Jms10Connection(delegate.createConnection()); + } + + @Override + public QueueConnection createQueueConnection(String userName, String password) + throws JMSException { + return new Jms10Connection(delegate.createConnection(userName, password)); + } + + // --- JMS 1.0 TopicConnectionFactory methods --- + + @Override + public TopicConnection createTopicConnection() throws JMSException { + return new Jms10Connection(delegate.createConnection()); + } + + @Override + public TopicConnection createTopicConnection(String userName, String password) + throws JMSException { + return new Jms10Connection(delegate.createConnection(userName, password)); + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueReceiver.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueReceiver.java new file mode 100644 index 00000000000..92b8f8ec93b --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueReceiver.java @@ -0,0 +1,59 @@ +package jms10mock; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Queue; +import javax.jms.QueueReceiver; + +/** Wraps a real {@link MessageConsumer} but simulates a JMS 1.0 provider. */ +public class Jms10QueueReceiver implements QueueReceiver { + private final MessageConsumer delegate; + private final Queue queue; + + public Jms10QueueReceiver(MessageConsumer delegate, Queue queue) { + this.delegate = delegate; + this.queue = queue; + } + + @Override + public Queue getQueue() { + return queue; + } + + @Override + public String getMessageSelector() throws JMSException { + return delegate.getMessageSelector(); + } + + @Override + public MessageListener getMessageListener() throws JMSException { + return delegate.getMessageListener(); + } + + @Override + public void setMessageListener(MessageListener listener) throws JMSException { + delegate.setMessageListener(listener); + } + + @Override + public Message receive() throws JMSException { + return delegate.receive(); + } + + @Override + public Message receive(long timeout) throws JMSException { + return delegate.receive(timeout); + } + + @Override + public Message receiveNoWait() throws JMSException { + return delegate.receiveNoWait(); + } + + @Override + public void close() throws JMSException { + delegate.close(); + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueSender.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueSender.java new file mode 100644 index 00000000000..1f888203483 --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueSender.java @@ -0,0 +1,124 @@ +package jms10mock; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.QueueSender; + +/** Wraps a real {@link MessageProducer} but simulates a JMS 1.0 provider. */ +public class Jms10QueueSender implements QueueSender { + private final MessageProducer delegate; + private final Queue queue; + + public Jms10QueueSender(MessageProducer delegate, Queue queue) { + this.delegate = delegate; + this.queue = queue; + } + + // --- JMS 1.1-only methods — not present in JMS 1.0 --- + + @Override + public Destination getDestination() { + throw new AbstractMethodError("JMS 1.0 provider does not implement getDestination()"); + } + + @Override + public void send(Destination destination, Message message) throws JMSException { + delegate.send(destination, message); + } + + @Override + public void send( + Destination destination, Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(destination, message, deliveryMode, priority, timeToLive); + } + + // --- JMS 1.0 QueueSender methods --- + + @Override + public Queue getQueue() { + return queue; + } + + @Override + public void send(Message message) throws JMSException { + delegate.send(message); + } + + @Override + public void send(Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(message, deliveryMode, priority, timeToLive); + } + + @Override + public void send(Queue queue, Message message) throws JMSException { + delegate.send(queue, message); + } + + @Override + public void send(Queue queue, Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(queue, message, deliveryMode, priority, timeToLive); + } + + // --- MessageProducer config methods --- + + @Override + public void close() throws JMSException { + delegate.close(); + } + + @Override + public void setDisableMessageID(boolean value) throws JMSException { + delegate.setDisableMessageID(value); + } + + @Override + public boolean getDisableMessageID() throws JMSException { + return delegate.getDisableMessageID(); + } + + @Override + public void setDisableMessageTimestamp(boolean value) throws JMSException { + delegate.setDisableMessageTimestamp(value); + } + + @Override + public boolean getDisableMessageTimestamp() throws JMSException { + return delegate.getDisableMessageTimestamp(); + } + + @Override + public void setDeliveryMode(int deliveryMode) throws JMSException { + delegate.setDeliveryMode(deliveryMode); + } + + @Override + public int getDeliveryMode() throws JMSException { + return delegate.getDeliveryMode(); + } + + @Override + public void setPriority(int defaultPriority) throws JMSException { + delegate.setPriority(defaultPriority); + } + + @Override + public int getPriority() throws JMSException { + return delegate.getPriority(); + } + + @Override + public void setTimeToLive(long timeToLive) throws JMSException { + delegate.setTimeToLive(timeToLive); + } + + @Override + public long getTimeToLive() throws JMSException { + return delegate.getTimeToLive(); + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Session.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Session.java new file mode 100644 index 00000000000..65629319237 --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Session.java @@ -0,0 +1,229 @@ +package jms10mock; + +import java.io.Serializable; +import javax.jms.BytesMessage; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MapMessage; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.MessageProducer; +import javax.jms.ObjectMessage; +import javax.jms.Queue; +import javax.jms.QueueBrowser; +import javax.jms.QueueReceiver; +import javax.jms.QueueSender; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.StreamMessage; +import javax.jms.TemporaryQueue; +import javax.jms.TemporaryTopic; +import javax.jms.TextMessage; +import javax.jms.Topic; +import javax.jms.TopicPublisher; +import javax.jms.TopicSession; +import javax.jms.TopicSubscriber; + +/** Wraps a real {@link Session} but simulates a JMS 1.0 provider. */ +public class Jms10Session implements QueueSession, TopicSession { + private final Session delegate; + + public Jms10Session(Session delegate) { + this.delegate = delegate; + } + + // --- JMS 1.1-only unified Session methods — not present in JMS 1.0 --- + + @Override + public MessageProducer createProducer(Destination destination) throws JMSException { + return delegate.createProducer(destination); + } + + @Override + public MessageConsumer createConsumer(Destination destination) throws JMSException { + return delegate.createConsumer(destination); + } + + @Override + public MessageConsumer createConsumer(Destination destination, String messageSelector) + throws JMSException { + return delegate.createConsumer(destination, messageSelector); + } + + @Override + public MessageConsumer createConsumer( + Destination destination, String messageSelector, boolean noLocal) throws JMSException { + return delegate.createConsumer(destination, messageSelector, noLocal); + } + + // --- JMS 1.0 QueueSession methods --- + + @Override + public Queue createQueue(String queueName) throws JMSException { + return delegate.createQueue(queueName); + } + + @Override + public QueueReceiver createReceiver(Queue queue) throws JMSException { + return new Jms10QueueReceiver(delegate.createConsumer(queue), queue); + } + + @Override + public QueueReceiver createReceiver(Queue queue, String messageSelector) throws JMSException { + return new Jms10QueueReceiver(delegate.createConsumer(queue, messageSelector), queue); + } + + @Override + public QueueSender createSender(Queue queue) throws JMSException { + return new Jms10QueueSender(delegate.createProducer(queue), queue); + } + + @Override + public QueueBrowser createBrowser(Queue queue) throws JMSException { + return delegate.createBrowser(queue); + } + + @Override + public QueueBrowser createBrowser(Queue queue, String messageSelector) throws JMSException { + return delegate.createBrowser(queue, messageSelector); + } + + @Override + public TemporaryQueue createTemporaryQueue() throws JMSException { + return delegate.createTemporaryQueue(); + } + + // --- JMS 1.0 TopicSession methods --- + + @Override + public Topic createTopic(String topicName) throws JMSException { + return delegate.createTopic(topicName); + } + + @Override + public TopicSubscriber createSubscriber(Topic topic) throws JMSException { + return new Jms10TopicSubscriber(delegate.createConsumer(topic), topic, false); + } + + @Override + public TopicSubscriber createSubscriber(Topic topic, String messageSelector, boolean noLocal) + throws JMSException { + return new Jms10TopicSubscriber( + delegate.createConsumer(topic, messageSelector, noLocal), topic, noLocal); + } + + @Override + public TopicSubscriber createDurableSubscriber(Topic topic, String name) throws JMSException { + return new Jms10TopicSubscriber(delegate.createDurableSubscriber(topic, name), topic, false); + } + + @Override + public TopicSubscriber createDurableSubscriber( + Topic topic, String name, String messageSelector, boolean noLocal) throws JMSException { + return new Jms10TopicSubscriber( + delegate.createDurableSubscriber(topic, name, messageSelector, noLocal), topic, noLocal); + } + + @Override + public TopicPublisher createPublisher(Topic topic) throws JMSException { + return new Jms10TopicPublisher(delegate.createProducer(topic), topic); + } + + @Override + public TemporaryTopic createTemporaryTopic() throws JMSException { + return delegate.createTemporaryTopic(); + } + + @Override + public void unsubscribe(String name) throws JMSException { + delegate.unsubscribe(name); + } + + // --- Common Session methods --- + + @Override + public BytesMessage createBytesMessage() throws JMSException { + return delegate.createBytesMessage(); + } + + @Override + public MapMessage createMapMessage() throws JMSException { + return delegate.createMapMessage(); + } + + @Override + public Message createMessage() throws JMSException { + return delegate.createMessage(); + } + + @Override + public ObjectMessage createObjectMessage() throws JMSException { + return delegate.createObjectMessage(); + } + + @Override + public ObjectMessage createObjectMessage(Serializable object) throws JMSException { + return delegate.createObjectMessage(object); + } + + @Override + public StreamMessage createStreamMessage() throws JMSException { + return delegate.createStreamMessage(); + } + + @Override + public TextMessage createTextMessage() throws JMSException { + return delegate.createTextMessage(); + } + + @Override + public TextMessage createTextMessage(String text) throws JMSException { + return delegate.createTextMessage(text); + } + + @Override + public boolean getTransacted() throws JMSException { + return delegate.getTransacted(); + } + + @Override + public int getAcknowledgeMode() { + throw new AbstractMethodError("JMS 1.0 provider does not implement getAcknowledgeMode()"); + } + + @Override + public void commit() throws JMSException { + delegate.commit(); + } + + @Override + public void rollback() throws JMSException { + delegate.rollback(); + } + + @Override + public void close() throws JMSException { + delegate.close(); + } + + @Override + public void recover() throws JMSException { + delegate.recover(); + } + + @Override + public MessageListener getMessageListener() throws JMSException { + return delegate.getMessageListener(); + } + + @Override + public void setMessageListener(MessageListener listener) throws JMSException { + delegate.setMessageListener(listener); + } + + @Override + public void run() { + delegate.run(); + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicPublisher.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicPublisher.java new file mode 100644 index 00000000000..6f3c1e38663 --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicPublisher.java @@ -0,0 +1,137 @@ +package jms10mock; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import javax.jms.Topic; +import javax.jms.TopicPublisher; + +/** Wraps a real {@link MessageProducer} but simulates a JMS 1.0 provider. */ +public class Jms10TopicPublisher implements TopicPublisher { + private final MessageProducer delegate; + private final Topic topic; + + public Jms10TopicPublisher(MessageProducer delegate, Topic topic) { + this.delegate = delegate; + this.topic = topic; + } + + // --- JMS 1.1-only methods — not present in JMS 1.0 --- + + @Override + public Destination getDestination() { + throw new AbstractMethodError("JMS 1.0 provider does not implement getDestination()"); + } + + @Override + public void send(Destination destination, Message message) throws JMSException { + delegate.send(destination, message); + } + + @Override + public void send( + Destination destination, Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(destination, message, deliveryMode, priority, timeToLive); + } + + // --- JMS 1.0 TopicPublisher methods --- + + @Override + public Topic getTopic() { + return topic; + } + + @Override + public void publish(Message message) throws JMSException { + delegate.send(message); + } + + @Override + public void publish(Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(message, deliveryMode, priority, timeToLive); + } + + @Override + public void publish(Topic topic, Message message) throws JMSException { + delegate.send(topic, message); + } + + @Override + public void publish(Topic topic, Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(topic, message, deliveryMode, priority, timeToLive); + } + + // --- MessageProducer send methods (also available via publish in 1.0) --- + + @Override + public void send(Message message) throws JMSException { + delegate.send(message); + } + + @Override + public void send(Message message, int deliveryMode, int priority, long timeToLive) + throws JMSException { + delegate.send(message, deliveryMode, priority, timeToLive); + } + + // --- MessageProducer config methods --- + + @Override + public void close() throws JMSException { + delegate.close(); + } + + @Override + public void setDisableMessageID(boolean value) throws JMSException { + delegate.setDisableMessageID(value); + } + + @Override + public boolean getDisableMessageID() throws JMSException { + return delegate.getDisableMessageID(); + } + + @Override + public void setDisableMessageTimestamp(boolean value) throws JMSException { + delegate.setDisableMessageTimestamp(value); + } + + @Override + public boolean getDisableMessageTimestamp() throws JMSException { + return delegate.getDisableMessageTimestamp(); + } + + @Override + public void setDeliveryMode(int deliveryMode) throws JMSException { + delegate.setDeliveryMode(deliveryMode); + } + + @Override + public int getDeliveryMode() throws JMSException { + return delegate.getDeliveryMode(); + } + + @Override + public void setPriority(int defaultPriority) throws JMSException { + delegate.setPriority(defaultPriority); + } + + @Override + public int getPriority() throws JMSException { + return delegate.getPriority(); + } + + @Override + public void setTimeToLive(long timeToLive) throws JMSException { + delegate.setTimeToLive(timeToLive); + } + + @Override + public long getTimeToLive() throws JMSException { + return delegate.getTimeToLive(); + } +} diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicSubscriber.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicSubscriber.java new file mode 100644 index 00000000000..97ca5ea2343 --- /dev/null +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicSubscriber.java @@ -0,0 +1,66 @@ +package jms10mock; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Topic; +import javax.jms.TopicSubscriber; + +/** Wraps a real {@link MessageConsumer} but simulates a JMS 1.0 provider. */ +public class Jms10TopicSubscriber implements TopicSubscriber { + private final MessageConsumer delegate; + private final Topic topic; + private final boolean noLocal; + + public Jms10TopicSubscriber(MessageConsumer delegate, Topic topic, boolean noLocal) { + this.delegate = delegate; + this.topic = topic; + this.noLocal = noLocal; + } + + @Override + public Topic getTopic() { + return topic; + } + + @Override + public boolean getNoLocal() { + return noLocal; + } + + @Override + public String getMessageSelector() throws JMSException { + return delegate.getMessageSelector(); + } + + @Override + public MessageListener getMessageListener() throws JMSException { + return delegate.getMessageListener(); + } + + @Override + public void setMessageListener(MessageListener listener) throws JMSException { + delegate.setMessageListener(listener); + } + + @Override + public Message receive() throws JMSException { + return delegate.receive(); + } + + @Override + public Message receive(long timeout) throws JMSException { + return delegate.receive(timeout); + } + + @Override + public Message receiveNoWait() throws JMSException { + return delegate.receiveNoWait(); + } + + @Override + public void close() throws JMSException { + delegate.close(); + } +} From 04a3a803823d378ec6667f33917068a59dac85c6 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 19 May 2026 14:00:55 +0200 Subject: [PATCH 2/4] move stubs to test fixture --- dd-java-agent/instrumentation/jms/javax-jms-1.1/build.gradle | 3 +++ .../{test => testFixtures}/java/jms10mock/Jms10Connection.java | 0 .../java/jms10mock/Jms10ConnectionFactory.java | 0 .../java/jms10mock/Jms10QueueReceiver.java | 0 .../java/jms10mock/Jms10QueueSender.java | 0 .../{test => testFixtures}/java/jms10mock/Jms10Session.java | 0 .../java/jms10mock/Jms10TopicPublisher.java | 0 .../java/jms10mock/Jms10TopicSubscriber.java | 0 8 files changed, 3 insertions(+) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10Connection.java (100%) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10ConnectionFactory.java (100%) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10QueueReceiver.java (100%) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10QueueSender.java (100%) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10Session.java (100%) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10TopicPublisher.java (100%) rename dd-java-agent/instrumentation/jms/javax-jms-1.1/src/{test => testFixtures}/java/jms10mock/Jms10TopicSubscriber.java (100%) diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/build.gradle b/dd-java-agent/instrumentation/jms/javax-jms-1.1/build.gradle index 739bc235846..74b9d050a9a 100644 --- a/dd-java-agent/instrumentation/jms/javax-jms-1.1/build.gradle +++ b/dd-java-agent/instrumentation/jms/javax-jms-1.1/build.gradle @@ -14,6 +14,7 @@ muzzle { } apply from: "$rootDir/gradle/java.gradle" +apply plugin: 'java-test-fixtures' repositories { maven { @@ -33,6 +34,8 @@ tasks.named("latestDepTest", Test) { dependencies { compileOnly group: 'javax.jms', name: 'jms-api', version: '1.1-rev-1' + testFixturesCompileOnly group: 'javax.jms', name: 'jms-api', version: '1.1-rev-1' + testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation') testImplementation group: 'org.apache.activemq.tooling', name: 'activemq-junit', version: '5.14.5' testImplementation group: 'org.apache.activemq', name: 'activemq-pool', version: '5.14.5' diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Connection.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10Connection.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Connection.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10Connection.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10ConnectionFactory.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10ConnectionFactory.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10ConnectionFactory.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10ConnectionFactory.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueReceiver.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10QueueReceiver.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueReceiver.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10QueueReceiver.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueSender.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10QueueSender.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10QueueSender.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10QueueSender.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Session.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10Session.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10Session.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10Session.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicPublisher.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10TopicPublisher.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicPublisher.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10TopicPublisher.java diff --git a/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicSubscriber.java b/dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10TopicSubscriber.java similarity index 100% rename from dd-java-agent/instrumentation/jms/javax-jms-1.1/src/test/java/jms10mock/Jms10TopicSubscriber.java rename to dd-java-agent/instrumentation/jms/javax-jms-1.1/src/testFixtures/java/jms10mock/Jms10TopicSubscriber.java From 91f239d760b4bf20ecdcd103a47f06849af2f16a Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Mon, 1 Jun 2026 10:23:20 -0400 Subject: [PATCH 3/4] eval: delete sparkjava-2.3 for blind test (v2, master 04a3a80382) --- .../spark/sparkjava-2.3/build.gradle | 24 --- .../spark/sparkjava-2.3/gradle.lockfile | 167 ------------------ .../sparkjava/RoutesInstrumentation.java | 58 ------ .../src/test/groovy/SparkJavaBasedTest.groovy | 73 -------- .../test/java/TestSparkJavaApplication.java | 19 -- 5 files changed, 341 deletions(-) delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle b/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle deleted file mode 100644 index d2c1dabe2a2..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ - -// building against 2.3 and testing against 2.4 because JettyHandler is available since 2.4 only -muzzle { - pass { - group = "com.sparkjava" - module = 'spark-core' - versions = "[2.3,)" - assertInverse = true - } -} - -apply from: "$rootDir/gradle/java.gradle" - -addTestSuiteForDir('latestDepTest', 'test') - -dependencies { - compileOnly group: 'com.sparkjava', name: 'spark-core', version: '2.3' - - testImplementation project(':dd-java-agent:instrumentation:jetty:jetty-server:jetty-server-9.0') - - testImplementation group: 'com.sparkjava', name: 'spark-core', version: '2.4' - - latestDepTestImplementation group: 'com.sparkjava', name: 'spark-core', version: '+' -} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile b/dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile deleted file mode 100644 index 03bd65d20d3..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile +++ /dev/null @@ -1,167 +0,0 @@ -# This is a Gradle generated file for dependency locking. -# Manual edits can break the build and are not advised. -# This file is expected to be part of source control. -cafe.cryptography:curve25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -cafe.cryptography:ed25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -ch.qos.logback:logback-classic:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -ch.qos.logback:logback-core:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okhttp3:okhttp:3.12.15=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okio:okio:1.17.6=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:dd-instrument-java:0.0.3=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:java-dogstatsd-client:4.4.5=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.datadoghq:sketches-java:0.8.3=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.javaparser:javaparser-core:3.25.6=codenarc -com.github.jnr:jffi:1.3.14=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-a64asm:1.0.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-constants:0.10.4=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-enxio:0.32.19=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-ffi:2.2.18=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-posix:3.1.21=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-unixsocket:0.38.24=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-x86asm:1.0.2=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath -com.github.spotbugs:spotbugs:4.9.8=spotbugs -com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs -com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,testAnnotationProcessor,testCompileClasspath -com.google.auto.service:auto-service:1.1.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.auto:auto-common:1.2.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.code.gson:gson:2.13.2=spotbugs -com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.errorprone:error_prone_annotations:2.41.0=spotbugs -com.google.guava:failureaccess:1.0.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.guava:guava:20.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:guava:32.0.1-jre=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.re2j:re2j:1.7=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.sparkjava:spark-core:2.3=compileClasspath -com.sparkjava:spark-core:2.4=testCompileClasspath,testRuntimeClasspath -com.sparkjava:spark-core:2.9.4=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -com.squareup.moshi:moshi:1.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:okhttp:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okio:okio:1.17.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.thoughtworks.qdox:qdox:1.12.1=codenarc -commons-fileupload:commons-fileupload:1.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.20.0=spotbugs -de.thetaphi:forbiddenapis:3.10=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -io.leangen.geantyref:geantyref:1.3.16=latestDepTestRuntimeClasspath,testRuntimeClasspath -io.sqreen:libsqreen:17.3.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -javax.servlet:javax.servlet-api:3.1.0=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -jaxen:jaxen:2.0.0=spotbugs -junit:junit:4.13.2=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.18.8=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.8=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna-platform:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.sf.saxon:Saxon-HE:12.9=spotbugs -org.apache.ant:ant-antlr:1.10.14=codenarc -org.apache.ant:ant-junit:1.10.14=codenarc -org.apache.bcel:bcel:6.11.0=spotbugs -org.apache.commons:commons-lang3:3.19.0=spotbugs -org.apache.commons:commons-text:1.14.0=spotbugs -org.apache.logging.log4j:log4j-api:2.25.2=spotbugs -org.apache.logging.log4j:log4j-core:2.25.2=spotbugs -org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath -org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -org.codehaus.groovy:groovy-ant:3.0.23=codenarc -org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc -org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.codehaus.groovy:groovy-templates:3.0.23=codenarc -org.codehaus.groovy:groovy-xml:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.codenarc:CodeNarc:3.7.0=codenarc -org.dom4j:dom4j:2.2.0=spotbugs -org.eclipse.jetty.websocket:websocket-api:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-api:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-api:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-client:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-client:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-client:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-common:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-common:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-common:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-server:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-server:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-server:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-servlet:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-servlet:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-servlet:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-client:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-http:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-http:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-http:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-io:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-io:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-io:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-security:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-security:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-security:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-server:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-server:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-server:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-servlet:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-servlet:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-servlet:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-util-ajax:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-util:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-util:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-util:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-webapp:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-webapp:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-webapp:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-xml:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-xml:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-xml:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.gmetrics:GMetrics:2.1.0=codenarc -org.hamcrest:hamcrest-core:1.3=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jctools:jctools-core-jdk11:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.jctools:jctools-core:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-launcher:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-runner:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-suite-api:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-suite-commons:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit:junit-bom:5.14.0=spotbugs -org.junit:junit-bom:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-core:4.4.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.objenesis:objenesis:3.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.ow2.asm:asm-analysis:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-analysis:9.9=spotbugs -org.ow2.asm:asm-commons:9.9=spotbugs -org.ow2.asm:asm-commons:9.9.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-tree:9.9=spotbugs -org.ow2.asm:asm-tree:9.9.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-util:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-util:9.9=spotbugs -org.ow2.asm:asm:9.9=spotbugs -org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:jcl-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:jul-to-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:log4j-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath -org.slf4j:slf4j-api:1.7.32=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j -org.slf4j:slf4j-simple:1.7.12=compileClasspath -org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j -org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -org.spockframework:spock-bom:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.spockframework:spock-core:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-junit:1.2.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-parser:1.2.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.xmlresolver:xmlresolver:5.3.3=spotbugs -empty=spotbugsPlugins diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java deleted file mode 100644 index b4dbe6e5c02..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java +++ /dev/null @@ -1,58 +0,0 @@ -package datadog.trace.instrumentation.sparkjava; - -import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; -import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; -import static net.bytebuddy.matcher.ElementMatchers.isPublic; -import static net.bytebuddy.matcher.ElementMatchers.returns; -import static net.bytebuddy.matcher.ElementMatchers.takesArgument; - -import com.google.auto.service.AutoService; -import datadog.trace.agent.tooling.Instrumenter; -import datadog.trace.agent.tooling.InstrumenterModule; -import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import net.bytebuddy.asm.Advice; -import spark.route.HttpMethod; -import spark.routematch.RouteMatch; - -@AutoService(InstrumenterModule.class) -public class RoutesInstrumentation extends InstrumenterModule.Tracing - implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { - - public RoutesInstrumentation() { - super("sparkjava", "sparkjava-2.4"); - } - - @Override - public boolean defaultEnabled() { - return false; - } - - @Override - public String instrumentedType() { - return "spark.route.Routes"; - } - - @Override - public void methodAdvice(MethodTransformer transformer) { - transformer.applyAdvice( - named("find") - .and(takesArgument(0, named("spark.route.HttpMethod"))) - .and(returns(named("spark.routematch.RouteMatch"))) - .and(isPublic()), - RoutesInstrumentation.class.getName() + "$RoutesAdvice"); - } - - public static class RoutesAdvice { - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void routeMatchEnricher( - @Advice.Argument(0) final HttpMethod method, @Advice.Return final RouteMatch routeMatch) { - - final AgentSpan span = activeSpan(); - if (span != null && routeMatch != null) { - HTTP_RESOURCE_DECORATOR.withRoute(span, method.name(), routeMatch.getMatchUri()); - } - } - } -} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy deleted file mode 100644 index 2c33e8d745b..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy +++ /dev/null @@ -1,73 +0,0 @@ -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.agent.test.utils.OkHttpUtils -import datadog.trace.agent.test.utils.PortUtils -import datadog.trace.api.DDSpanTypes -import datadog.trace.bootstrap.instrumentation.api.Tags -import okhttp3.OkHttpClient -import okhttp3.Request -import spark.Spark -import spock.lang.Shared - -class SparkJavaBasedTest extends InstrumentationSpecification { - - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("dd.integration.jetty.enabled", "true") - injectSysConfig("dd.integration.sparkjava.enabled", "true") - } - - @Shared - int port - - OkHttpClient client = OkHttpUtils.client() - - def setupSpec() { - port = PortUtils.randomOpenPort() - TestSparkJavaApplication.initSpark(port) - } - - def cleanupSpec() { - Spark.stop() - } - - def "generates spans"() { - setup: - def request = new Request.Builder() - .url("http://localhost:$port/param/asdf1234") - .get() - .build() - def response = client.newCall(request).execute() - - expect: - port != 0 - response.body().string() == "Hello asdf1234" - - assertTraces(1) { - trace(1) { - span { - operationName "servlet.request" - resourceName "GET /param/:param" - spanType DDSpanTypes.HTTP_SERVER - errored false - parent() - tags { - "$Tags.COMPONENT" "jetty-server" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER - "$Tags.PEER_HOST_IPV4" "127.0.0.1" - "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" "http://localhost:$port/param/asdf1234" - "$Tags.HTTP_HOSTNAME" "localhost" - "$Tags.HTTP_METHOD" "GET" - "$Tags.HTTP_STATUS" 200 - "$Tags.HTTP_ROUTE" String - "$Tags.HTTP_USER_AGENT" String - "$Tags.HTTP_CLIENT_IP" "127.0.0.1" - "$Tags.NETWORK_CLIENT_IP" "127.0.0.1" - defaultTags() - } - } - } - } - } -} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java deleted file mode 100644 index 93f904c7206..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java +++ /dev/null @@ -1,19 +0,0 @@ -import spark.Spark; - -public class TestSparkJavaApplication { - - public static void initSpark(final int port) { - Spark.port(port); - Spark.get("/", (req, res) -> "Hello World"); - - Spark.get("/param/:param", (req, res) -> "Hello " + req.params("param")); - - Spark.get( - "/exception/:param", - (req, res) -> { - throw new RuntimeException(req.params("param")); - }); - - Spark.awaitInitialization(); - } -} From eae4c58ec0b03a87ff55b8686fe22f74ece50848 Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Thu, 4 Jun 2026 10:08:09 -0400 Subject: [PATCH 4/4] feat(sparkjava-2.3): toolkit-generated sparkjava-2.3 instrumentation v3 [DO NOT MERGE] Generated by APM Instrumentation Toolkit new_integration workflow. Reviewer approved (todos_fixed=1, todos_remaining=0). Cost: ~$8.28. Java tests: SparkJavaTest.java, SparkJavaForkedTest.java. R20 fix applied (Java tests, no Groovy). 1 reviewer iter vs v2's 10. Blind test on master 04a3a80382. --- .../spark/sparkjava-2.3/build.gradle | 27 + .../sparkjava/RoutesInstrumentation.java | 63 ++ .../sparkjava/SparkJavaDecorator.java | 31 + .../sparkjava/SparkJavaForkedTest.java | 226 ++++++ .../sparkjava/SparkJavaTest.java | 687 ++++++++++++++++++ 5 files changed, 1034 insertions(+) create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle b/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle new file mode 100644 index 00000000000..14ce833d991 --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle @@ -0,0 +1,27 @@ +// building against 2.3 and testing against 2.4 because JettyHandler is available since 2.4 only +muzzle { + pass { + group = "com.sparkjava" + module = 'spark-core' + versions = "[2.3,)" + assertInverse = true + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly group: 'com.sparkjava', name: 'spark-core', version: '2.3' + + testImplementation project(':dd-java-agent:instrumentation:jetty:jetty-server:jetty-server-9.0') + + testImplementation group: 'com.sparkjava', name: 'spark-core', version: '2.4' + + latestDepTestImplementation group: 'com.sparkjava', name: 'spark-core', version: '+' +} + +tasks.withType(Test).configureEach { + jvmArgs += ['-Ddd.trace.enabled=true'] +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java new file mode 100644 index 00000000000..3534719bea7 --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java @@ -0,0 +1,63 @@ +package datadog.trace.instrumentation.sparkjava; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; +import static datadog.trace.instrumentation.sparkjava.SparkJavaDecorator.DECORATE; +import static datadog.trace.instrumentation.sparkjava.SparkJavaDecorator.SPARK_JAVA; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import net.bytebuddy.asm.Advice; +import spark.route.HttpMethod; +import spark.routematch.RouteMatch; + +@AutoService(InstrumenterModule.class) +public class RoutesInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public RoutesInstrumentation() { + super("sparkjava", "sparkjava-2.3"); + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".SparkJavaDecorator"}; + } + + @Override + public String instrumentedType() { + return "spark.route.Routes"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("find") + .and(takesArgument(0, named("spark.route.HttpMethod"))) + .and(returns(named("spark.routematch.RouteMatch"))) + .and(isPublic()), + RoutesInstrumentation.class.getName() + "$RoutesAdvice"); + } + + public static class RoutesAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void routeMatchEnricher( + @Advice.Argument(0) final HttpMethod method, @Advice.Return final RouteMatch routeMatch) { + + final AgentSpan span = activeSpan(); + if (span != null && routeMatch != null) { + HTTP_RESOURCE_DECORATOR.withRoute(span, method.name(), routeMatch.getMatchUri()); + span.setSpanName(DECORATE.spanName()); + span.setTag(Tags.COMPONENT, SPARK_JAVA); + } + } + } +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java new file mode 100644 index 00000000000..4657f85ccad --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java @@ -0,0 +1,31 @@ +package datadog.trace.instrumentation.sparkjava; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator; + +public class SparkJavaDecorator extends BaseDecorator { + + public static final SparkJavaDecorator DECORATE = new SparkJavaDecorator(); + + public static final CharSequence SPARK_JAVA = UTF8BytesString.create("spark-java"); + public static final CharSequence SPARK_REQUEST = UTF8BytesString.create("spark.request"); + + @Override + protected String[] instrumentationNames() { + return new String[] {"sparkjava"}; + } + + @Override + protected CharSequence spanType() { + return "web"; + } + + @Override + protected CharSequence component() { + return SPARK_JAVA; + } + + public CharSequence spanName() { + return SPARK_REQUEST; + } +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java new file mode 100644 index 00000000000..9b5de61b72b --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java @@ -0,0 +1,226 @@ +package datadog.trace.instrumentation.sparkjava; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.agent.test.utils.PortUtils; +import datadog.trace.core.DDSpan; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import spark.Request; +import spark.Response; +import spark.Route; +import spark.Spark; + +/** + * Forked test for the SparkJava 2.x instrumentation, running in an isolated JVM. This validates + * that the {@link RoutesInstrumentation} loads and enriches Jetty server spans correctly when the + * agent starts from scratch — no leftover state from other test classes. + * + *

This test focuses on the core enrichment contract: when a request matches a SparkJava route, + * the server span gets operation name {@code spark.request}, component {@code spark-java}, and the + * resource name / http.route reflect the parameterized route pattern. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SparkJavaForkedTest extends AbstractInstrumentationTest { + + private int actualPort; + + @BeforeAll + void setupServer() { + actualPort = PortUtils.randomOpenPort(); + Spark.port(actualPort); + + Spark.get( + "/ping", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "pong"; + } + }); + + Spark.get( + "/items/:id", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("application/json"); + return "{\"id\": \"" + request.params(":id") + "\"}"; + } + }); + + Spark.get( + "/fail", + new Route() { + @Override + public Object handle(Request request, Response response) { + throw new RuntimeException("Forked test error"); + } + }); + + Spark.awaitInitialization(); + } + + @AfterAll + void tearDownServer() throws InterruptedException { + Spark.stop(); + Thread.sleep(500); + } + + @Test + void simpleRouteEnrichesServerSpan() throws InterruptedException, TimeoutException { + httpGet("/ping"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/ping", 200, false); + } + + @Test + void parameterizedRoutePatternInResourceName() throws InterruptedException, TimeoutException { + httpGet("/items/42"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/items/:id", 200, false); + } + + @Test + void errorRouteProducesErrorSpan() throws InterruptedException, TimeoutException { + httpGet("/fail"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/fail", 500, true); + } + + // --------------------------------------------------------------- + // Helper methods + // --------------------------------------------------------------- + + /** + * Validates the complete structure of a server span, covering both SparkJava enrichment and the + * underlying Jetty server span baseline. This single-point-of-assertion prevents regressions when + * new required tags are added. + * + *

SparkJava enrichment (set by {@link RoutesInstrumentation}): + * + *

+ * + *

Jetty baseline (set by the Jetty server instrumentation): + * + *

+ * + * @param span the server span to validate + * @param httpMethod the expected HTTP method (e.g., "GET", "POST") + * @param route the expected route pattern (e.g., "/items/:id") + * @param statusCode the expected HTTP status code + * @param isError whether the span should be marked as errored + */ + private void assertServerSpan( + DDSpan span, String httpMethod, String route, int statusCode, boolean isError) { + assertNotNull(span, "Expected a server span for " + httpMethod + " " + route); + + // SparkJava enrichment assertions + assertEquals( + "spark.request", + span.getOperationName().toString(), + "Operation name should be 'spark.request'"); + assertEquals( + "spark-java", + String.valueOf(span.getTag("component")), + "component tag should be 'spark-java'"); + assertEquals( + httpMethod + " " + route, + span.getResourceName().toString(), + "Resource name should be HTTP_METHOD + route_pattern"); + assertEquals( + route, + String.valueOf(span.getTag("http.route")), + "http.route should contain the route pattern, not the actual path"); + + // Jetty baseline assertions + assertEquals("web", span.getSpanType(), "Span type should be 'web'"); + assertEquals( + "server", String.valueOf(span.getTag("span.kind")), "span.kind should be 'server'"); + assertEquals(httpMethod, String.valueOf(span.getTag("http.method")), "http.method tag"); + assertEquals(statusCode, span.getTag("http.status_code"), "http.status_code tag"); + assertNotNull(span.getTag("http.url"), "http.url tag should be set"); + assertEquals(isError, span.isError(), "error flag"); + } + + /** + * Waits for at least one trace to be written and returns the server span. + * + * @return the server span (never null — fails assertion if not found) + * @throws InterruptedException if the thread is interrupted while waiting + * @throws TimeoutException if no trace is written within the timeout + */ + private DDSpan waitForServerSpan() throws InterruptedException, TimeoutException { + writer.waitForTraces(1); + List spans = new ArrayList<>(); + for (List trace : writer) { + spans.addAll(trace); + } + DDSpan serverSpan = null; + for (DDSpan span : spans) { + if ("server".equals(String.valueOf(span.getTag("span.kind"))) + || "web".equals(span.getSpanType())) { + serverSpan = span; + break; + } + } + assertNotNull(serverSpan, "Expected to find a server span in the collected traces"); + return serverSpan; + } + + /** + * Makes an HTTP GET request to the SparkJava server. + * + * @param path the request path + * @return the HTTP status code + */ + private int httpGet(String path) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + int status = conn.getResponseCode(); + InputStream is = + conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream(); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + while (reader.readLine() != null) { + // drain + } + reader.close(); + } + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP GET failed for path " + path, e); + } + } +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java new file mode 100644 index 00000000000..6abafc9051b --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java @@ -0,0 +1,687 @@ +package datadog.trace.instrumentation.sparkjava; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.agent.test.utils.PortUtils; +import datadog.trace.api.DDTraceId; +import datadog.trace.core.DDSpan; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import spark.Request; +import spark.Response; +import spark.Route; +import spark.Spark; + +/** + * Tests for the SparkJava 2.x HTTP server instrumentation. + * + *

SparkJava runs on an embedded Jetty server. The Jetty instrumentation creates the server span, + * and the SparkJava {@link RoutesInstrumentation} enriches it with route information from the + * {@code Routes.find()} method. + * + *

Acceptance criteria verified by these tests: + * + *

    + *
  • A server span is created for each HTTP request handled by a SparkJava route + *
  • The operation name is set to {@code spark.request} + *
  • The span type is {@code web} and span.kind is {@code server} + *
  • The component tag is set to {@code spark-java} + *
  • The resource name is enriched to {@code HTTP_METHOD route_pattern} (e.g., {@code GET + * /hello/:name}) + *
  • The http.route tag contains the parameterized route pattern, not the concrete path + *
  • HTTP tags (method, URL, status code) are set correctly + *
  • Error routes (500) set the error flag on the span + *
  • Unmatched routes (404) retain Jetty defaults — no SparkJava enrichment fires + *
  • Context propagation via Datadog headers links server spans to parent traces + *
+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SparkJavaTest extends AbstractInstrumentationTest { + + private int actualPort; + + @BeforeAll + void setupServer() { + actualPort = PortUtils.randomOpenPort(); + Spark.port(actualPort); + + Spark.get( + "/hello", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "Hello, World!"; + } + }); + + Spark.get( + "/hello/:name", + new Route() { + @Override + public Object handle(Request request, Response response) { + String name = request.params(":name"); + response.type("text/plain"); + return "Hello, " + name + "!"; + } + }); + + Spark.post( + "/users", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("application/json"); + response.status(201); + return "{\"created\": true}"; + } + }); + + Spark.put( + "/users/:id", + new Route() { + @Override + public Object handle(Request request, Response response) { + String id = request.params(":id"); + response.type("application/json"); + return "{\"updated\": true, \"id\": \"" + id + "\"}"; + } + }); + + Spark.delete( + "/users/:id", + new Route() { + @Override + public Object handle(Request request, Response response) { + String id = request.params(":id"); + response.type("application/json"); + return "{\"deleted\": true, \"id\": \"" + id + "\"}"; + } + }); + + Spark.get( + "/error", + new Route() { + @Override + public Object handle(Request request, Response response) { + throw new RuntimeException("Intentional error for testing"); + } + }); + + Spark.get( + "/files/*", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "file content for " + request.splat()[0]; + } + }); + + Spark.before( + "/filtered/*", + new spark.Filter() { + @Override + public void handle(Request request, Response response) { + response.header("X-Filtered", "true"); + } + }); + + Spark.get( + "/filtered/resource", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "filtered response"; + } + }); + + Spark.after( + "/after-filtered/*", + new spark.Filter() { + @Override + public void handle(Request request, Response response) { + response.header("X-After-Filtered", "true"); + } + }); + + Spark.get( + "/after-filtered/resource", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "after-filtered response"; + } + }); + + Spark.awaitInitialization(); + } + + @AfterAll + void tearDownServer() throws InterruptedException { + Spark.stop(); + Thread.sleep(500); + } + + // --------------------------------------------------------------- + // Route enrichment tests — verify SparkJava sets operation name, + // component, resource name, and http.route on the Jetty server span + // --------------------------------------------------------------- + + @Test + void getRouteCreatesServerSpanWithCorrectTags() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/hello", 200, false); + } + + @Test + void getRouteWithPathParamUsesParameterizedRoutePattern() + throws InterruptedException, TimeoutException { + httpGet("/hello/spark-user"); + + DDSpan serverSpan = waitForServerSpan(); + // The route pattern should be /hello/:name (parameterized), not /hello/spark-user (actual path) + assertServerSpan(serverSpan, "GET", "/hello/:name", 200, false); + } + + @Test + void postRouteCreatesServerSpanWithCorrectStatusCode() + throws InterruptedException, TimeoutException { + httpRequest("/users", "POST", "test-body"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "POST", "/users", 201, false); + } + + @Test + void putRouteWithPathParamCreatesServerSpan() throws InterruptedException, TimeoutException { + httpRequest("/users/42", "PUT", "update-body"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "PUT", "/users/:id", 200, false); + } + + @Test + void deleteRouteWithPathParamCreatesServerSpan() throws InterruptedException, TimeoutException { + httpRequest("/users/99", "DELETE", null); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "DELETE", "/users/:id", 200, false); + } + + @Test + void wildcardRouteUsesWildcardPattern() throws InterruptedException, TimeoutException { + httpGet("/files/documents/report.pdf"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/files/*", 200, false); + } + + @Test + void beforeFilterDoesNotInterfereWithRouteEnrichment() + throws InterruptedException, TimeoutException { + httpGet("/filtered/resource"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/filtered/resource", 200, false); + } + + @Test + void afterFilterDoesNotInterfereWithSpanData() throws InterruptedException, TimeoutException { + httpGet("/after-filtered/resource"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/after-filtered/resource", 200, false); + } + + // --------------------------------------------------------------- + // Span structure tests — verify individual span attributes + // --------------------------------------------------------------- + + @Test + void serverSpanHasCorrectType() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + assertEquals("web", serverSpan.getSpanType(), "HTTP server spans should have type 'web'"); + assertEquals( + "server", + String.valueOf(serverSpan.getTag("span.kind")), + "Span kind should be 'server' for HTTP server spans"); + } + + @Test + void serverSpanHasCorrectOperationName() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + assertEquals( + "spark.request", + serverSpan.getOperationName().toString(), + "Operation name should be 'spark.request' for SparkJava routes"); + } + + @Test + void serverSpanIncludesHttpUrlTag() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + String httpUrl = String.valueOf(serverSpan.getTag("http.url")); + assertNotNull(httpUrl, "Expected http.url tag to be set"); + assertTrue( + httpUrl.contains("/hello"), + "http.url tag should contain the request path, got: " + httpUrl); + assertTrue(httpUrl.startsWith("http"), "http.url tag should be a full URL, got: " + httpUrl); + } + + // --------------------------------------------------------------- + // Error handling tests + // --------------------------------------------------------------- + + @Test + void errorRouteCreatesServerSpanWithErrorFlag() throws InterruptedException, TimeoutException { + httpGet("/error"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/error", 500, true); + // SparkJava catches exceptions internally via its ExceptionMapper before they propagate + // to Jetty. The error flag is set solely from the 500 status code by Jetty's + // HttpServerDecorator.onResponse(). Because the exception never reaches the Jetty handler, + // error.type/error.message/error.stack are not populated on the span. + assertNull( + serverSpan.getTag("error.type"), + "error.type should not be set — SparkJava catches exceptions before Jetty sees them"); + assertNull( + serverSpan.getTag("error.message"), + "error.message should not be set — SparkJava catches exceptions before Jetty sees them"); + assertNull( + serverSpan.getTag("error.stack"), + "error.stack should not be set — SparkJava catches exceptions before Jetty sees them"); + } + + @Test + void notFoundRouteCreates404Span() throws InterruptedException, TimeoutException { + httpGet("/nonexistent"); + + DDSpan serverSpan = waitForServerSpan(); + // For 404, Routes.find() returns null so SparkJava enrichment does not fire. + // The span retains Jetty defaults — no http.route or spark-java component tag is expected. + // We can't use assertServerSpan() here because it asserts SparkJava-specific enrichment + // (operation name, component, http.route) that won't be present on an unmatched route. + assertEquals("web", serverSpan.getSpanType(), "Span type should be 'web' even for 404"); + assertEquals( + "server", String.valueOf(serverSpan.getTag("span.kind")), "span.kind should be 'server'"); + assertEquals(404, serverSpan.getTag("http.status_code"), "http.status_code should be 404"); + assertEquals("GET", String.valueOf(serverSpan.getTag("http.method")), "http.method tag"); + assertNotNull(serverSpan.getTag("http.url"), "http.url tag should be set even for 404"); + assertEquals(false, serverSpan.isError(), "404 should not be marked as an error"); + } + + // --------------------------------------------------------------- + // Context propagation tests + // --------------------------------------------------------------- + + @Test + void contextPropagationLinksServerSpanToParentTrace() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "123456789"); + headers.put("x-datadog-parent-id", "987654321"); + httpGetWithHeaders("/hello", headers); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/hello", 200, false); + assertEquals( + DDTraceId.from("123456789"), + serverSpan.getTraceId(), + "Server span should inherit the trace ID from the propagated Datadog headers"); + assertEquals( + 987654321L, + serverSpan.getParentId(), + "Server span's parent ID should match the x-datadog-parent-id header value"); + } + + @Test + void contextPropagationPreservesSparkJavaRouteEnrichment() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "111111111"); + headers.put("x-datadog-parent-id", "222222222"); + httpGetWithHeaders("/hello", headers); + + DDSpan serverSpan = waitForServerSpan(); + // Verify SparkJava route enrichment still works with propagated context + assertServerSpan(serverSpan, "GET", "/hello", 200, false); + // Verify context propagation + assertEquals( + DDTraceId.from("111111111"), + serverSpan.getTraceId(), + "Trace ID should be inherited from propagated headers"); + assertEquals(222222222L, serverSpan.getParentId()); + } + + @Test + void contextPropagationWorksWithParameterizedRoutes() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "333333333"); + headers.put("x-datadog-parent-id", "444444444"); + httpGetWithHeaders("/hello/sparkuser", headers); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/hello/:name", 200, false); + assertEquals( + DDTraceId.from("333333333"), + serverSpan.getTraceId(), + "Trace ID should be inherited from propagated headers"); + assertEquals( + 444444444L, serverSpan.getParentId(), "Parent ID should match propagated header value"); + } + + @Test + void contextPropagationPreservesErrorStatusOnErrorRoutes() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "555555555"); + headers.put("x-datadog-parent-id", "666666666"); + httpGetWithHeaders("/error", headers); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/error", 500, true); + assertEquals( + DDTraceId.from("555555555"), + serverSpan.getTraceId(), + "Trace ID should be inherited even for error routes"); + assertEquals( + 666666666L, + serverSpan.getParentId(), + "Parent ID should match propagated header even for error routes"); + } + + @Test + void differentPropagatedContextsProduceDistinctTraces() + throws InterruptedException, TimeoutException { + Map headers1 = new HashMap<>(); + headers1.put("x-datadog-trace-id", "100000001"); + headers1.put("x-datadog-parent-id", "200000001"); + httpGetWithHeaders("/hello", headers1); + + Map headers2 = new HashMap<>(); + headers2.put("x-datadog-trace-id", "100000002"); + headers2.put("x-datadog-parent-id", "200000002"); + httpGetWithHeaders("/hello", headers2); + + writer.waitForTraces(2); + List allSpans = flattenTraces(); + + // Find both server spans + DDSpan firstServerSpan = null; + DDSpan secondServerSpan = null; + for (DDSpan span : allSpans) { + if ("server".equals(String.valueOf(span.getTag("span.kind"))) + || "web".equals(span.getSpanType())) { + if (DDTraceId.from("100000001").equals(span.getTraceId())) { + firstServerSpan = span; + } else if (DDTraceId.from("100000002").equals(span.getTraceId())) { + secondServerSpan = span; + } + } + } + + assertNotNull(firstServerSpan, "Expected server span for first request (trace 100000001)"); + assertNotNull(secondServerSpan, "Expected server span for second request (trace 100000002)"); + + // Verify each span links to its own propagated context + assertNotEquals( + firstServerSpan.getTraceId(), + secondServerSpan.getTraceId(), + "Each request should have its own distinct trace ID from propagated context"); + assertEquals(200000001L, firstServerSpan.getParentId()); + assertEquals(200000002L, secondServerSpan.getParentId()); + + // Both should still have correct route enrichment + assertEquals("GET /hello", firstServerSpan.getResourceName().toString()); + assertEquals("GET /hello", secondServerSpan.getResourceName().toString()); + } + + // --------------------------------------------------------------- + // Helper methods + // --------------------------------------------------------------- + + /** + * Waits for at least one trace to be written, then finds and returns the server span. This + * combines the common pattern of waiting + flattening + finding into a single call, reducing + * boilerplate in test methods. + * + * @return the server span (never null — fails assertion if not found) + * @throws InterruptedException if the thread is interrupted while waiting + * @throws TimeoutException if no trace is written within the timeout + */ + private DDSpan waitForServerSpan() throws InterruptedException, TimeoutException { + writer.waitForTraces(1); + List spans = flattenTraces(); + DDSpan serverSpan = findServerSpan(spans); + assertNotNull(serverSpan, "Expected to find a server span in the collected traces"); + return serverSpan; + } + + /** + * Flattens all collected traces into a single list of spans for easier assertion. + * + * @return all spans from all collected traces + */ + private List flattenTraces() { + List result = new ArrayList<>(); + for (List trace : writer) { + result.addAll(trace); + } + return result; + } + + /** + * Finds the server span in the list of spans. The server span is identified by having {@code + * span.kind=server} or by having a {@code web} span type. + * + * @param spans the list of spans to search + * @return the server span, or {@code null} if not found + */ + private DDSpan findServerSpan(List spans) { + for (DDSpan span : spans) { + if ("server".equals(String.valueOf(span.getTag("span.kind"))) + || "web".equals(span.getSpanType())) { + return span; + } + } + return null; + } + + /** + * Validates the complete structure of a server span, covering both SparkJava enrichment and the + * underlying Jetty server span baseline. This single-point-of-assertion prevents regressions when + * new required tags are added. + * + *

SparkJava enrichment (set by {@link RoutesInstrumentation}): + * + *

    + *
  • operation name = {@code spark.request} + *
  • component = {@code spark-java} + *
  • resource name = {@code HTTP_METHOD route_pattern} + *
  • http.route = parameterized route pattern + *
+ * + *

Jetty baseline (set by the Jetty server instrumentation): + * + *

    + *
  • span type = {@code web} + *
  • span.kind = {@code server} + *
  • http.method, http.status_code, http.url + *
  • error flag (from HTTP status code) + *
+ * + * @param span the server span to validate + * @param httpMethod the expected HTTP method (e.g., "GET", "POST") + * @param route the expected route pattern (e.g., "/hello/:name") + * @param statusCode the expected HTTP status code + * @param isError whether the span should be marked as errored + */ + private void assertServerSpan( + DDSpan span, String httpMethod, String route, int statusCode, boolean isError) { + assertNotNull(span, "Expected a server span for " + httpMethod + " " + route); + + // SparkJava enrichment assertions + assertEquals( + "spark.request", + span.getOperationName().toString(), + "Operation name should be 'spark.request'"); + assertEquals( + "spark-java", + String.valueOf(span.getTag("component")), + "component tag should be 'spark-java'"); + assertEquals( + httpMethod + " " + route, + span.getResourceName().toString(), + "Resource name should be HTTP_METHOD + route_pattern"); + assertEquals( + route, + String.valueOf(span.getTag("http.route")), + "http.route should contain the route pattern, not the actual path"); + + // Jetty baseline assertions + assertEquals("web", span.getSpanType(), "Span type should be 'web'"); + assertEquals( + "server", String.valueOf(span.getTag("span.kind")), "span.kind should be 'server'"); + assertEquals(httpMethod, String.valueOf(span.getTag("http.method")), "http.method tag"); + assertEquals(statusCode, span.getTag("http.status_code"), "http.status_code tag"); + assertNotNull(span.getTag("http.url"), "http.url tag should be set"); + assertEquals(isError, span.isError(), "error flag"); + } + + /** + * Makes an HTTP GET request to the SparkJava server with custom headers. Used for context + * propagation tests to inject Datadog trace headers (e.g., {@code x-datadog-trace-id}, {@code + * x-datadog-parent-id}) that simulate an upstream service propagating its trace context. + * + * @param path the request path (e.g., {@code /hello}) + * @param headers map of header name to value to set on the request + * @return the HTTP status code + */ + private int httpGetWithHeaders(String path, Map headers) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + int status = conn.getResponseCode(); + drainResponse(conn); + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP GET failed for path " + path, e); + } + } + + /** + * Makes an HTTP GET request to the SparkJava server. + * + * @param path the request path (e.g., {@code /hello}) + * @return the HTTP status code + */ + private int httpGet(String path) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + int status = conn.getResponseCode(); + drainResponse(conn); + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP GET failed for path " + path, e); + } + } + + /** + * Makes an HTTP request with the specified method and optional body. + * + * @param path the request path + * @param method the HTTP method (e.g., POST, PUT, DELETE) + * @param body the request body, or {@code null} for no body + * @return the HTTP status code + */ + private int httpRequest(String path, String method, String body) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + if (body != null) { + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "text/plain"); + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes("UTF-8")); + } + } + + int status = conn.getResponseCode(); + drainResponse(conn); + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP " + method + " failed for path " + path, e); + } + } + + /** + * Drains the response body to ensure the server-side processing completes fully before the + * connection is closed. + * + * @param conn the HTTP connection to drain + */ + private void drainResponse(HttpURLConnection conn) { + try { + InputStream is = + conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream(); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + while (reader.readLine() != null) { + // drain + } + reader.close(); + } + } catch (Exception ignored) { + // ignore drain errors + } + } +}