From d19d27e4854b6a561885f0db13c99131bcc446d9 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 28 Jul 2025 16:20:24 -0400 Subject: [PATCH 01/31] Add RumInjectorHealthMetrics --- .../DefaultRumInjectorHealthMetrics.java | 120 ++++++++++ .../monitor/RumInjectorHealthMetrics.java | 23 ++ .../RumInjectorHealthMetricsTest.groovy | 224 ++++++++++++++++++ 3 files changed, 367 insertions(+) create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java new file mode 100644 index 00000000000..a171bdd6486 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java @@ -0,0 +1,120 @@ +package datadog.trace.core.monitor; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import datadog.trace.api.StatsDClient; +import datadog.trace.api.rum.RumTelemetryCollector; +import datadog.trace.util.AgentTaskScheduler; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.jctools.counters.CountersFactory; +import org.jctools.counters.FixedSizeStripedLongCounter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Default implementation of RumInjectorHealthMetrics that reports metrics to StatsDClient +public class DefaultRumInjectorHealthMetrics extends RumInjectorHealthMetrics + implements RumTelemetryCollector { + private static final Logger log = LoggerFactory.getLogger(DefaultRumInjectorHealthMetrics.class); + + private static final String[] NO_TAGS = new String[0]; + + private final AtomicBoolean started = new AtomicBoolean(false); + private volatile AgentTaskScheduler.Scheduled cancellation; + + private final FixedSizeStripedLongCounter injectionSucceed = + CountersFactory.createFixedSizeStripedCounter(8); + private final FixedSizeStripedLongCounter injectionFailed = + CountersFactory.createFixedSizeStripedCounter(8); + private final FixedSizeStripedLongCounter injectionSkipped = + CountersFactory.createFixedSizeStripedCounter(8); + + private final StatsDClient statsd; + private final long interval; + private final TimeUnit units; + + @Override + public void start() { + if (started.compareAndSet(false, true)) { + cancellation = + AgentTaskScheduler.INSTANCE.scheduleAtFixedRate( + new Flush(), this, interval, interval, units); + } + } + + public DefaultRumInjectorHealthMetrics(final StatsDClient statsd) { + this(statsd, 30, SECONDS); + } + + public DefaultRumInjectorHealthMetrics(final StatsDClient statsd, long interval, TimeUnit units) { + this.statsd = statsd; + this.interval = interval; + this.units = units; + } + + @Override + public void onInjectionSucceed() { + injectionSucceed.inc(); + } + + @Override + public void onInjectionFailed() { + injectionFailed.inc(); + } + + @Override + public void onInjectionSkipped() { + injectionSkipped.inc(); + } + + @Override + public void close() { + if (null != cancellation) { + cancellation.cancel(); + } + } + + @Override + public String summary() { + return "injectionSucceed=" + + injectionSucceed.get() + + "\ninjectionFailed=" + + injectionFailed.get() + + "\ninjectionSkipped=" + + injectionSkipped.get(); + } + + private static class Flush implements AgentTaskScheduler.Task { + + private final long[] previousCounts = new long[3]; // one per counter + private int countIndex; + + @Override + public void run(DefaultRumInjectorHealthMetrics target) { + countIndex = -1; + try { + reportIfChanged(target.statsd, "rum.injection.succeed", target.injectionSucceed, NO_TAGS); + reportIfChanged(target.statsd, "rum.injection.failed", target.injectionFailed, NO_TAGS); + reportIfChanged(target.statsd, "rum.injection.skipped", target.injectionSkipped, NO_TAGS); + } catch (ArrayIndexOutOfBoundsException e) { + log.warn( + "previousCounts array needs resizing to at least {}, was {}", + countIndex + 1, + previousCounts.length); + } + } + + private void reportIfChanged( + StatsDClient statsDClient, + String aspect, + FixedSizeStripedLongCounter counter, + String[] tags) { + long count = counter.get(); + long delta = count - previousCounts[++countIndex]; + if (delta > 0) { + statsDClient.count(aspect, delta, tags); + previousCounts[countIndex] = count; + } + } + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java new file mode 100644 index 00000000000..aa950e2d491 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java @@ -0,0 +1,23 @@ +package datadog.trace.core.monitor; + +// Abstract health metrics for RUM injector +// This class defines the interface for monitoring RUM injection operations +public abstract class RumInjectorHealthMetrics implements AutoCloseable { + public static RumInjectorHealthMetrics NO_OP = new RumInjectorHealthMetrics() {}; + + public void start() {} + + public void onInjectionSucceed() {} + + public void onInjectionFailed() {} + + public void onInjectionSkipped() {} + + /** @return Human-readable summary of the current health metrics. */ + public String summary() { + return ""; + } + + @Override + public void close() {} +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy new file mode 100644 index 00000000000..2733d985b96 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy @@ -0,0 +1,224 @@ +package datadog.trace.core.monitor + +import datadog.trace.api.StatsDClient +import spock.lang.Specification +import spock.lang.Subject + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class RumInjectorHealthMetricsTest extends Specification { + def statsD = Mock(StatsDClient) + + @Subject + def healthMetrics = new DefaultRumInjectorHealthMetrics(statsD) + + def "test onInjectionSucceed"() { + setup: + def latch = new CountDownLatch(1) + def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + healthMetrics.start() + + when: + healthMetrics.onInjectionSucceed() + latch.await(5, TimeUnit.SECONDS) + + then: + 1 * statsD.count('rum.injection.succeed', 1, _) + 0 * _ + + cleanup: + healthMetrics.close() + } + + def "test onInjectionFailed"() { + setup: + def latch = new CountDownLatch(1) + def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + healthMetrics.start() + + when: + healthMetrics.onInjectionFailed() + latch.await(5, TimeUnit.SECONDS) + + then: + 1 * statsD.count('rum.injection.failed', 1, _) + 0 * _ + + cleanup: + healthMetrics.close() + } + + def "test onInjectionSkipped"() { + setup: + def latch = new CountDownLatch(1) + def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + healthMetrics.start() + + when: + healthMetrics.onInjectionSkipped() + latch.await(5, TimeUnit.SECONDS) + + then: + 1 * statsD.count('rum.injection.skipped', 1, _) + 0 * _ + + cleanup: + healthMetrics.close() + } + + def "test multiple events"() { + setup: + def latch = new CountDownLatch(3) // expecting 3 metric types + def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + healthMetrics.start() + + when: + healthMetrics.onInjectionSucceed() + healthMetrics.onInjectionFailed() + healthMetrics.onInjectionSkipped() + latch.await(5, TimeUnit.SECONDS) + + then: + 1 * statsD.count('rum.injection.succeed', 1, _) + 1 * statsD.count('rum.injection.failed', 1, _) + 1 * statsD.count('rum.injection.skipped', 1, _) + 0 * _ + + cleanup: + healthMetrics.close() + } + + def "test summary"() { + when: + healthMetrics.onInjectionSucceed() + healthMetrics.onInjectionFailed() + healthMetrics.onInjectionSkipped() + def summary = healthMetrics.summary() + + then: + summary.contains("injectionSucceed=1") + summary.contains("injectionFailed=1") + summary.contains("injectionSkipped=1") + 0 * _ + } + + // taken from HealthMetricsTest + private static class Latched implements StatsDClient { + final StatsDClient delegate + final CountDownLatch latch + + Latched(StatsDClient delegate, CountDownLatch latch) { + this.delegate = delegate + this.latch = latch + } + + @Override + void incrementCounter(String metricName, String... tags) { + try { + delegate.incrementCounter(metricName, tags) + } finally { + latch.countDown() + } + } + + @Override + void count(String metricName, long delta, String... tags) { + try { + delegate.count(metricName, delta, tags) + } finally { + latch.countDown() + } + } + + @Override + void gauge(String metricName, long value, String... tags) { + try { + delegate.gauge(metricName, value, tags) + } finally { + latch.countDown() + } + } + + @Override + void gauge(String metricName, double value, String... tags) { + try { + delegate.gauge(metricName, value, tags) + } finally { + latch.countDown() + } + } + + @Override + void histogram(String metricName, long value, String... tags) { + try { + delegate.histogram(metricName, value, tags) + } finally { + latch.countDown() + } + } + + @Override + void histogram(String metricName, double value, String... tags) { + try { + delegate.histogram(metricName, value, tags) + } finally { + latch.countDown() + } + } + + @Override + void distribution(String metricName, long value, String... tags) { + try { + delegate.distribution(metricName, value, tags) + } finally { + latch.countDown() + } + } + + @Override + void distribution(String metricName, double value, String... tags) { + try { + delegate.distribution(metricName, value, tags) + } finally { + latch.countDown() + } + } + + @Override + void serviceCheck(String serviceCheckName, String status, String message, String... tags) { + try { + delegate.serviceCheck(serviceCheckName, status, message, tags) + } finally { + latch.countDown() + } + } + + @Override + void error(Exception error) { + try { + delegate.error(error) + } finally { + latch.countDown() + } + } + + @Override + int getErrorCount() { + try { + return delegate.getErrorCount() + } finally { + latch.countDown() + } + } + + @Override + void close() { + try { + delegate.close() + } finally { + latch.countDown() + } + } + } +} From e01d1a92c48d3f029a587e84326732026de1638b Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 28 Jul 2025 16:27:00 -0400 Subject: [PATCH 02/31] Add telemetry collector and methods to RumInjector --- .../datadog/trace/api/rum/RumInjector.java | 22 ++++++++++++++++ .../trace/api/rum/RumTelemetryCollector.java | 26 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index 5501c80e62c..939f495b3fd 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -29,6 +29,9 @@ public final class RumInjector { private final DDCache markerCache; private final Function snippetBytes; + // Health metrics telemetry collector (set by CoreTracer) + private static volatile RumTelemetryCollector telemetryCollector = RumTelemetryCollector.NO_OP; + RumInjector(Config config, InstrumenterConfig instrumenterConfig) { boolean rumEnabled = instrumenterConfig.isRumEnabled(); RumInjectorConfig injectorConfig = config.getRumInjectorConfig(); @@ -122,4 +125,23 @@ public byte[] getMarkerBytes(String encoding) { } return this.markerCache.computeIfAbsent(encoding, MARKER_BYTES); } + + public static void setTelemetryCollector(RumTelemetryCollector collector) { + telemetryCollector = collector != null ? collector : RumTelemetryCollector.NO_OP; + } + + // report that the RUM injector succeeded in injecting the SDK in an HTTP response + public static void reportInjectionSucceed() { + telemetryCollector.onInjectionSucceed(); + } + + // report that the RUM injector failed to inject the SDK in an HTTP response + public static void reportInjectionFailed() { + telemetryCollector.onInjectionFailed(); + } + + // report that the RUM injector skipped injecting the SDK in an HTTP response + public static void reportInjectionSkipped() { + telemetryCollector.onInjectionSkipped(); + } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java new file mode 100644 index 00000000000..78dc8341de8 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -0,0 +1,26 @@ +package datadog.trace.api.rum; + +// Collect RUM injection telemetry +public interface RumTelemetryCollector { + + RumTelemetryCollector NO_OP = + new RumTelemetryCollector() { + @Override + public void onInjectionSucceed() {} + + @Override + public void onInjectionFailed() {} + + @Override + public void onInjectionSkipped() {} + }; + + // call when RUM injection succeeds + void onInjectionSucceed(); + + // call when RUM injection fails + void onInjectionFailed(); + + // call when RUM injection is skipped + void onInjectionSkipped(); +} From 8c71ef6db072a84b0634ce5dc2db95dd8d73f020 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 28 Jul 2025 16:28:11 -0400 Subject: [PATCH 03/31] Initialize health metrics and telemetry collector --- .../main/java/datadog/trace/core/CoreTracer.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 6b5ea0b5f10..3eb9371e4fa 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -49,6 +49,7 @@ import datadog.trace.api.metrics.SpanMetricRegistry; import datadog.trace.api.naming.SpanNaming; import datadog.trace.api.remoteconfig.ServiceNameCollector; +import datadog.trace.api.rum.RumInjector; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.scopemanager.ScopeListener; import datadog.trace.api.time.SystemTimeSource; @@ -84,8 +85,10 @@ import datadog.trace.core.datastreams.DefaultDataStreamsMonitoring; import datadog.trace.core.flare.TracerFlarePoller; import datadog.trace.core.histogram.Histograms; +import datadog.trace.core.monitor.DefaultRumInjectorHealthMetrics; import datadog.trace.core.monitor.HealthMetrics; import datadog.trace.core.monitor.MonitoringImpl; +import datadog.trace.core.monitor.RumInjectorHealthMetrics; import datadog.trace.core.monitor.TracerHealthMetrics; import datadog.trace.core.propagation.ExtractedContext; import datadog.trace.core.propagation.HttpCodec; @@ -205,6 +208,7 @@ public static CoreTracerBuilder builder() { private final Monitoring performanceMonitoring; private final HealthMetrics healthMetrics; + private final RumInjectorHealthMetrics rumInjectorHealthMetrics; private final Recording traceWriteTimer; private final IdGenerationStrategy idGenerationStrategy; private final TraceCollector.Factory traceCollectorFactory; @@ -703,6 +707,15 @@ private CoreTracer( ? new TracerHealthMetrics(this.statsDClient) : HealthMetrics.NO_OP; healthMetrics.start(); + + rumInjectorHealthMetrics = + config.isHealthMetricsEnabled() && config.isRumEnabled() + ? new DefaultRumInjectorHealthMetrics(this.statsDClient) + : RumInjectorHealthMetrics.NO_OP; + rumInjectorHealthMetrics.start(); + // Register health metrics telemetry collector with RumInjector + RumInjector.setTelemetryCollector((DefaultRumInjectorHealthMetrics) rumInjectorHealthMetrics); + performanceMonitoring = config.isPerfMetricsEnabled() ? new MonitoringImpl(this.statsDClient, 10, SECONDS) @@ -1248,6 +1261,7 @@ public void close() { tracingConfigPoller.stop(); pendingTraceBuffer.close(); writer.close(); + rumInjectorHealthMetrics.close(); statsDClient.close(); metricsAggregator.close(); dataStreamsMonitoring.close(); From 6c01e1063e52c659fb66db88d1a1136e707725c8 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 28 Jul 2025 16:28:57 -0400 Subject: [PATCH 04/31] Get injectionsucceed count --- .../instrumentation/servlet3/RumHttpServletResponseWrapper.java | 1 + .../instrumentation/servlet5/RumHttpServletResponseWrapper.java | 1 + 2 files changed, 2 insertions(+) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index a5defc56dfe..01ca9c12b91 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -117,6 +117,7 @@ public void resetBuffer() { } public void onInjected() { + RumInjector.reportInjectionSucceed(); try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 4b91afd3890..fe7bb4c6e96 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -94,6 +94,7 @@ public void resetBuffer() { } public void onInjected() { + RumInjector.reportInjectionSucceed(); try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { From ceb3e11dbf6f063e01ada85d41d199269acfb1c7 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 28 Jul 2025 17:11:35 -0400 Subject: [PATCH 05/31] Add comments --- .../src/main/java/datadog/trace/core/CoreTracer.java | 3 ++- .../core/monitor/DefaultRumInjectorHealthMetrics.java | 4 +++- .../trace/core/monitor/RumInjectorHealthMetrics.java | 3 +-- .../main/java/datadog/trace/api/rum/RumInjector.java | 10 +++++++--- .../datadog/trace/api/rum/RumTelemetryCollector.java | 3 ++- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 3eb9371e4fa..23d2e17cdf4 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -708,12 +708,13 @@ private CoreTracer( : HealthMetrics.NO_OP; healthMetrics.start(); + // Start RUM injector health metrics rumInjectorHealthMetrics = config.isHealthMetricsEnabled() && config.isRumEnabled() ? new DefaultRumInjectorHealthMetrics(this.statsDClient) : RumInjectorHealthMetrics.NO_OP; rumInjectorHealthMetrics.start(); - // Register health metrics telemetry collector with RumInjector + // Register rumInjectorHealthMetrics as the RumInjector's telemetry collector RumInjector.setTelemetryCollector((DefaultRumInjectorHealthMetrics) rumInjectorHealthMetrics); performanceMonitoring = diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java index a171bdd6486..fe31374ed29 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java @@ -12,7 +12,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// Default implementation of RumInjectorHealthMetrics that reports metrics to StatsDClient +// Default implementation of RumInjectorHealthMetrics that reports metrics via StatsDClient +// This class implements the RumTelemetryCollector interface, which is used to collect telemetry +// from the RumInjector in the internal-api module public class DefaultRumInjectorHealthMetrics extends RumInjectorHealthMetrics implements RumTelemetryCollector { private static final Logger log = LoggerFactory.getLogger(DefaultRumInjectorHealthMetrics.class); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java index aa950e2d491..067778e36c5 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java @@ -1,7 +1,6 @@ package datadog.trace.core.monitor; -// Abstract health metrics for RUM injector -// This class defines the interface for monitoring RUM injection operations +// Abstract health metrics class for RUM injector public abstract class RumInjectorHealthMetrics implements AutoCloseable { public static RumInjectorHealthMetrics NO_OP = new RumInjectorHealthMetrics() {}; diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index 939f495b3fd..08e693b9bf8 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -126,21 +126,25 @@ public byte[] getMarkerBytes(String encoding) { return this.markerCache.computeIfAbsent(encoding, MARKER_BYTES); } + // set the telemetry collector for the RumInjector public static void setTelemetryCollector(RumTelemetryCollector collector) { telemetryCollector = collector != null ? collector : RumTelemetryCollector.NO_OP; } - // report that the RUM injector succeeded in injecting the SDK in an HTTP response + // report to the telemetry collector that the RUM injector succeeded in injecting the SDK in an + // HTTP response public static void reportInjectionSucceed() { telemetryCollector.onInjectionSucceed(); } - // report that the RUM injector failed to inject the SDK in an HTTP response + // report to the telemetry collector that the RUM injector failed to inject the SDK in an HTTP + // response public static void reportInjectionFailed() { telemetryCollector.onInjectionFailed(); } - // report that the RUM injector skipped injecting the SDK in an HTTP response + // report to the telemetry collector that the RUM injector skipped injecting the SDK in an HTTP + // response public static void reportInjectionSkipped() { telemetryCollector.onInjectionSkipped(); } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 78dc8341de8..8c79a796e41 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -1,6 +1,7 @@ package datadog.trace.api.rum; -// Collect RUM injection telemetry +// Collect RUM injection telemetry from the RumInjector +// This is implemented by the DefaultRumInjectorHealthMetrics class in the dd-trace-core module public interface RumTelemetryCollector { RumTelemetryCollector NO_OP = From 92f0c54d918a46b94a651312334372cd4614e122 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Wed, 30 Jul 2025 21:54:17 -0400 Subject: [PATCH 06/31] Reorganize classes --- .../java/datadog/trace/core/CoreTracer.java | 14 +++--- .../monitor/RumInjectorHealthMetrics.java | 22 ---------- .../trace/api/rum/RumInjectorMetrics.java | 44 +++++++------------ .../api/rum/RumInjectorMetricsTest.groovy | 14 +++--- 4 files changed, 30 insertions(+), 64 deletions(-) delete mode 100644 dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java rename dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java => internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java (65%) rename dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy => internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy (88%) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 23d2e17cdf4..064059438fa 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -50,6 +50,8 @@ import datadog.trace.api.naming.SpanNaming; import datadog.trace.api.remoteconfig.ServiceNameCollector; import datadog.trace.api.rum.RumInjector; +import datadog.trace.api.rum.RumInjectorMetrics; +import datadog.trace.api.rum.RumTelemetryCollector; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.scopemanager.ScopeListener; import datadog.trace.api.time.SystemTimeSource; @@ -85,10 +87,8 @@ import datadog.trace.core.datastreams.DefaultDataStreamsMonitoring; import datadog.trace.core.flare.TracerFlarePoller; import datadog.trace.core.histogram.Histograms; -import datadog.trace.core.monitor.DefaultRumInjectorHealthMetrics; import datadog.trace.core.monitor.HealthMetrics; import datadog.trace.core.monitor.MonitoringImpl; -import datadog.trace.core.monitor.RumInjectorHealthMetrics; import datadog.trace.core.monitor.TracerHealthMetrics; import datadog.trace.core.propagation.ExtractedContext; import datadog.trace.core.propagation.HttpCodec; @@ -208,7 +208,7 @@ public static CoreTracerBuilder builder() { private final Monitoring performanceMonitoring; private final HealthMetrics healthMetrics; - private final RumInjectorHealthMetrics rumInjectorHealthMetrics; + private final RumTelemetryCollector rumInjectorHealthMetrics; private final Recording traceWriteTimer; private final IdGenerationStrategy idGenerationStrategy; private final TraceCollector.Factory traceCollectorFactory; @@ -708,14 +708,14 @@ private CoreTracer( : HealthMetrics.NO_OP; healthMetrics.start(); - // Start RUM injector health metrics + // Start RUM injector metrics rumInjectorHealthMetrics = config.isHealthMetricsEnabled() && config.isRumEnabled() - ? new DefaultRumInjectorHealthMetrics(this.statsDClient) - : RumInjectorHealthMetrics.NO_OP; + ? new RumInjectorMetrics(this.statsDClient) + : RumTelemetryCollector.NO_OP; rumInjectorHealthMetrics.start(); // Register rumInjectorHealthMetrics as the RumInjector's telemetry collector - RumInjector.setTelemetryCollector((DefaultRumInjectorHealthMetrics) rumInjectorHealthMetrics); + RumInjector.setTelemetryCollector(rumInjectorHealthMetrics); performanceMonitoring = config.isPerfMetricsEnabled() diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java deleted file mode 100644 index 067778e36c5..00000000000 --- a/dd-trace-core/src/main/java/datadog/trace/core/monitor/RumInjectorHealthMetrics.java +++ /dev/null @@ -1,22 +0,0 @@ -package datadog.trace.core.monitor; - -// Abstract health metrics class for RUM injector -public abstract class RumInjectorHealthMetrics implements AutoCloseable { - public static RumInjectorHealthMetrics NO_OP = new RumInjectorHealthMetrics() {}; - - public void start() {} - - public void onInjectionSucceed() {} - - public void onInjectionFailed() {} - - public void onInjectionSkipped() {} - - /** @return Human-readable summary of the current health metrics. */ - public String summary() { - return ""; - } - - @Override - public void close() {} -} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java similarity index 65% rename from dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java rename to internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index fe31374ed29..312d7f20914 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/monitor/DefaultRumInjectorHealthMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -1,41 +1,34 @@ -package datadog.trace.core.monitor; +package datadog.trace.api.rum; import static java.util.concurrent.TimeUnit.SECONDS; import datadog.trace.api.StatsDClient; -import datadog.trace.api.rum.RumTelemetryCollector; import datadog.trace.util.AgentTaskScheduler; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import org.jctools.counters.CountersFactory; -import org.jctools.counters.FixedSizeStripedLongCounter; +import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // Default implementation of RumInjectorHealthMetrics that reports metrics via StatsDClient // This class implements the RumTelemetryCollector interface, which is used to collect telemetry // from the RumInjector in the internal-api module -public class DefaultRumInjectorHealthMetrics extends RumInjectorHealthMetrics - implements RumTelemetryCollector { - private static final Logger log = LoggerFactory.getLogger(DefaultRumInjectorHealthMetrics.class); +public class RumInjectorMetrics implements RumTelemetryCollector { + private static final Logger log = LoggerFactory.getLogger(RumInjectorMetrics.class); private static final String[] NO_TAGS = new String[0]; private final AtomicBoolean started = new AtomicBoolean(false); - private volatile AgentTaskScheduler.Scheduled cancellation; + private volatile AgentTaskScheduler.Scheduled cancellation; - private final FixedSizeStripedLongCounter injectionSucceed = - CountersFactory.createFixedSizeStripedCounter(8); - private final FixedSizeStripedLongCounter injectionFailed = - CountersFactory.createFixedSizeStripedCounter(8); - private final FixedSizeStripedLongCounter injectionSkipped = - CountersFactory.createFixedSizeStripedCounter(8); + private final AtomicLong injectionSucceed = new AtomicLong(); + private final AtomicLong injectionFailed = new AtomicLong(); + private final AtomicLong injectionSkipped = new AtomicLong(); private final StatsDClient statsd; private final long interval; private final TimeUnit units; - @Override public void start() { if (started.compareAndSet(false, true)) { cancellation = @@ -44,11 +37,11 @@ public void start() { } } - public DefaultRumInjectorHealthMetrics(final StatsDClient statsd) { + public RumInjectorMetrics(final StatsDClient statsd) { this(statsd, 30, SECONDS); } - public DefaultRumInjectorHealthMetrics(final StatsDClient statsd, long interval, TimeUnit units) { + public RumInjectorMetrics(final StatsDClient statsd, long interval, TimeUnit units) { this.statsd = statsd; this.interval = interval; this.units = units; @@ -56,27 +49,25 @@ public DefaultRumInjectorHealthMetrics(final StatsDClient statsd, long interval, @Override public void onInjectionSucceed() { - injectionSucceed.inc(); + injectionSucceed.incrementAndGet(); } @Override public void onInjectionFailed() { - injectionFailed.inc(); + injectionFailed.incrementAndGet(); } @Override public void onInjectionSkipped() { - injectionSkipped.inc(); + injectionSkipped.incrementAndGet(); } - @Override public void close() { if (null != cancellation) { cancellation.cancel(); } } - @Override public String summary() { return "injectionSucceed=" + injectionSucceed.get() @@ -86,13 +77,13 @@ public String summary() { + injectionSkipped.get(); } - private static class Flush implements AgentTaskScheduler.Task { + private static class Flush implements AgentTaskScheduler.Task { private final long[] previousCounts = new long[3]; // one per counter private int countIndex; @Override - public void run(DefaultRumInjectorHealthMetrics target) { + public void run(RumInjectorMetrics target) { countIndex = -1; try { reportIfChanged(target.statsd, "rum.injection.succeed", target.injectionSucceed, NO_TAGS); @@ -107,10 +98,7 @@ public void run(DefaultRumInjectorHealthMetrics target) { } private void reportIfChanged( - StatsDClient statsDClient, - String aspect, - FixedSizeStripedLongCounter counter, - String[] tags) { + StatsDClient statsDClient, String aspect, AtomicLong counter, String[] tags) { long count = counter.get(); long delta = count - previousCounts[++countIndex]; if (delta > 0) { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy similarity index 88% rename from dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy rename to internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 2733d985b96..4c49d7d1974 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/monitor/RumInjectorHealthMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -1,4 +1,4 @@ -package datadog.trace.core.monitor +package datadog.trace.api.rum import datadog.trace.api.StatsDClient import spock.lang.Specification @@ -7,16 +7,16 @@ import spock.lang.Subject import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -class RumInjectorHealthMetricsTest extends Specification { +class RumInjectorMetricsTest extends Specification { def statsD = Mock(StatsDClient) @Subject - def healthMetrics = new DefaultRumInjectorHealthMetrics(statsD) + def healthMetrics = new RumInjectorMetrics(statsD) def "test onInjectionSucceed"() { setup: def latch = new CountDownLatch(1) - def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) healthMetrics.start() when: @@ -34,7 +34,7 @@ class RumInjectorHealthMetricsTest extends Specification { def "test onInjectionFailed"() { setup: def latch = new CountDownLatch(1) - def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) healthMetrics.start() when: @@ -52,7 +52,7 @@ class RumInjectorHealthMetricsTest extends Specification { def "test onInjectionSkipped"() { setup: def latch = new CountDownLatch(1) - def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) healthMetrics.start() when: @@ -70,7 +70,7 @@ class RumInjectorHealthMetricsTest extends Specification { def "test multiple events"() { setup: def latch = new CountDownLatch(3) // expecting 3 metric types - def healthMetrics = new DefaultRumInjectorHealthMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) healthMetrics.start() when: From c5c860fafcfc27cabff7a8e0b2ac1e499286fd6a Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Wed, 30 Jul 2025 22:37:29 -0400 Subject: [PATCH 07/31] Connect rum injector, telemetry collector, and statsdclient --- .../RumHttpServletResponseWrapper.java | 2 +- .../RumHttpServletResponseWrapper.java | 2 +- .../java/datadog/trace/core/CoreTracer.java | 17 ++---- .../datadog/trace/api/rum/RumInjector.java | 36 ++++++------ .../trace/api/rum/RumInjectorMetrics.java | 3 +- .../trace/api/rum/RumTelemetryCollector.java | 22 +++++++- .../trace/api/rum/RumInjectorTest.groovy | 55 +++++++++++++++++++ 7 files changed, 104 insertions(+), 33 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index 01ca9c12b91..c73065a3894 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -117,7 +117,7 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.reportInjectionSucceed(); + RumInjector.getTelemetryCollector().onInjectionSucceed(); try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index fe7bb4c6e96..1f37f9df4ad 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -94,7 +94,7 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.reportInjectionSucceed(); + RumInjector.getTelemetryCollector().onInjectionSucceed(); try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 064059438fa..abb240a75ae 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -50,8 +50,6 @@ import datadog.trace.api.naming.SpanNaming; import datadog.trace.api.remoteconfig.ServiceNameCollector; import datadog.trace.api.rum.RumInjector; -import datadog.trace.api.rum.RumInjectorMetrics; -import datadog.trace.api.rum.RumTelemetryCollector; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.scopemanager.ScopeListener; import datadog.trace.api.time.SystemTimeSource; @@ -208,7 +206,6 @@ public static CoreTracerBuilder builder() { private final Monitoring performanceMonitoring; private final HealthMetrics healthMetrics; - private final RumTelemetryCollector rumInjectorHealthMetrics; private final Recording traceWriteTimer; private final IdGenerationStrategy idGenerationStrategy; private final TraceCollector.Factory traceCollectorFactory; @@ -708,14 +705,10 @@ private CoreTracer( : HealthMetrics.NO_OP; healthMetrics.start(); - // Start RUM injector metrics - rumInjectorHealthMetrics = - config.isHealthMetricsEnabled() && config.isRumEnabled() - ? new RumInjectorMetrics(this.statsDClient) - : RumTelemetryCollector.NO_OP; - rumInjectorHealthMetrics.start(); - // Register rumInjectorHealthMetrics as the RumInjector's telemetry collector - RumInjector.setTelemetryCollector(rumInjectorHealthMetrics); + // Start RUM injector telemetry + if (config.isRumEnabled()) { + RumInjector.enableTelemetry(this.statsDClient); + } performanceMonitoring = config.isPerfMetricsEnabled() @@ -1262,7 +1255,7 @@ public void close() { tracingConfigPoller.stop(); pendingTraceBuffer.close(); writer.close(); - rumInjectorHealthMetrics.close(); + RumInjector.shutdownTelemetry(); statsDClient.close(); metricsAggregator.close(); dataStreamsMonitoring.close(); diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index 08e693b9bf8..6c424b20759 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -29,7 +29,7 @@ public final class RumInjector { private final DDCache markerCache; private final Function snippetBytes; - // Health metrics telemetry collector (set by CoreTracer) + // telemetry collector defaults to NO_OP private static volatile RumTelemetryCollector telemetryCollector = RumTelemetryCollector.NO_OP; RumInjector(Config config, InstrumenterConfig instrumenterConfig) { @@ -126,26 +126,30 @@ public byte[] getMarkerBytes(String encoding) { return this.markerCache.computeIfAbsent(encoding, MARKER_BYTES); } - // set the telemetry collector for the RumInjector - public static void setTelemetryCollector(RumTelemetryCollector collector) { - telemetryCollector = collector != null ? collector : RumTelemetryCollector.NO_OP; + // start telemetry collection and report metrics via the given StatsDClient + public static void enableTelemetry(datadog.trace.api.StatsDClient statsDClient) { + if (statsDClient != null) { + RumInjectorMetrics metrics = new RumInjectorMetrics(statsDClient); + metrics.start(); + telemetryCollector = metrics; + } else { + telemetryCollector = RumTelemetryCollector.NO_OP; + } } - // report to the telemetry collector that the RUM injector succeeded in injecting the SDK in an - // HTTP response - public static void reportInjectionSucceed() { - telemetryCollector.onInjectionSucceed(); + // shutdown telemetry and reset to NO_OP + public static void shutdownTelemetry() { + telemetryCollector.close(); + telemetryCollector = RumTelemetryCollector.NO_OP; } - // report to the telemetry collector that the RUM injector failed to inject the SDK in an HTTP - // response - public static void reportInjectionFailed() { - telemetryCollector.onInjectionFailed(); + // set the telemetry collector + public static void setTelemetryCollector(RumTelemetryCollector collector) { + telemetryCollector = collector != null ? collector : RumTelemetryCollector.NO_OP; } - // report to the telemetry collector that the RUM injector skipped injecting the SDK in an HTTP - // response - public static void reportInjectionSkipped() { - telemetryCollector.onInjectionSkipped(); + // get the telemetry collector. this is used to directly report telemetry + public static RumTelemetryCollector getTelemetryCollector() { + return telemetryCollector; } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 312d7f20914..38a622512c3 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -10,9 +10,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// Default implementation of RumInjectorHealthMetrics that reports metrics via StatsDClient // This class implements the RumTelemetryCollector interface, which is used to collect telemetry -// from the RumInjector in the internal-api module +// from the RumInjector. Metrics are then reported via StatsDClient. public class RumInjectorMetrics implements RumTelemetryCollector { private static final Logger log = LoggerFactory.getLogger(RumInjectorMetrics.class); diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 8c79a796e41..5806f3f157e 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -1,11 +1,14 @@ package datadog.trace.api.rum; // Collect RUM injection telemetry from the RumInjector -// This is implemented by the DefaultRumInjectorHealthMetrics class in the dd-trace-core module +// This is implemented by the RumInjectorMetrics class public interface RumTelemetryCollector { RumTelemetryCollector NO_OP = new RumTelemetryCollector() { + @Override + public void start() {} + @Override public void onInjectionSucceed() {} @@ -14,8 +17,18 @@ public void onInjectionFailed() {} @Override public void onInjectionSkipped() {} + + @Override + public void close() {} + + @Override + public String summary() { + return ""; + } }; + default void start() {} + // call when RUM injection succeeds void onInjectionSucceed(); @@ -24,4 +37,11 @@ public void onInjectionSkipped() {} // call when RUM injection is skipped void onInjectionSkipped(); + + default void close() {} + + // human-readable summary of the current health metrics + default String summary() { + return ""; + } } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index 98988e40242..ce09ad814e4 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -65,4 +65,59 @@ class RumInjectorTest extends DDSpecification { injector.getSnippetChars() != null injector.getMarkerChars() != null } + + void 'set telemetry collector'() { + setup: + def mockTelemetryCollector = mock(RumTelemetryCollector) + + when: + RumInjector.setTelemetryCollector(mockTelemetryCollector) + def telemetryCollector = RumInjector.getTelemetryCollector() + + then: + telemetryCollector == mockTelemetryCollector + } + + void 'return NO_OP when telemetry collector is not set'() { + when: + RumInjector.setTelemetryCollector(null) + def telemetryCollector = RumInjector.getTelemetryCollector() + + then: + telemetryCollector == RumTelemetryCollector.NO_OP + } + + void 'enable telemetry with StatsDClient'() { + setup: + def mockStatsDClient = mock(datadog.trace.api.StatsDClient) + RumInjector.enableTelemetry(mockStatsDClient) + + when: + def telemetryCollector = RumInjector.getTelemetryCollector() + + then: + telemetryCollector instanceof datadog.trace.api.rum.RumInjectorMetrics + } + + void 'enabling telemetry with a null StatsDClient sets the telemetry collector to NO_OP'() { + when: + RumInjector.enableTelemetry(null) + def telemetryCollector = RumInjector.getTelemetryCollector() + + then: + telemetryCollector == RumTelemetryCollector.NO_OP + } + + void 'shutdown telemetry'() { + setup: + def mockStatsDClient = mock(datadog.trace.api.StatsDClient) + RumInjector.enableTelemetry(mockStatsDClient) + + when: + RumInjector.shutdownTelemetry() + def telemetryCollector = RumInjector.getTelemetryCollector() + + then: + telemetryCollector == RumTelemetryCollector.NO_OP + } } From 3d4ac5387277eef6dc8841db984e6068017101d5 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 31 Jul 2025 22:22:36 -0400 Subject: [PATCH 08/31] Add tests --- .../api/rum/RumInjectorMetricsTest.groovy | 90 +++++++++++++------ .../trace/api/rum/RumInjectorTest.groovy | 88 ++++++++++++++---- 2 files changed, 134 insertions(+), 44 deletions(-) diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 4c49d7d1974..baae9a5a8d3 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -11,16 +11,16 @@ class RumInjectorMetricsTest extends Specification { def statsD = Mock(StatsDClient) @Subject - def healthMetrics = new RumInjectorMetrics(statsD) + def metrics = new RumInjectorMetrics(statsD) def "test onInjectionSucceed"() { setup: def latch = new CountDownLatch(1) - def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - healthMetrics.start() + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() when: - healthMetrics.onInjectionSucceed() + metrics.onInjectionSucceed() latch.await(5, TimeUnit.SECONDS) then: @@ -28,17 +28,17 @@ class RumInjectorMetricsTest extends Specification { 0 * _ cleanup: - healthMetrics.close() + metrics.close() } def "test onInjectionFailed"() { setup: def latch = new CountDownLatch(1) - def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - healthMetrics.start() + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() when: - healthMetrics.onInjectionFailed() + metrics.onInjectionFailed() latch.await(5, TimeUnit.SECONDS) then: @@ -46,17 +46,17 @@ class RumInjectorMetricsTest extends Specification { 0 * _ cleanup: - healthMetrics.close() + metrics.close() } def "test onInjectionSkipped"() { setup: def latch = new CountDownLatch(1) - def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - healthMetrics.start() + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() when: - healthMetrics.onInjectionSkipped() + metrics.onInjectionSkipped() latch.await(5, TimeUnit.SECONDS) then: @@ -64,19 +64,19 @@ class RumInjectorMetricsTest extends Specification { 0 * _ cleanup: - healthMetrics.close() + metrics.close() } - def "test multiple events"() { + def "test flushing multiple events"() { setup: def latch = new CountDownLatch(3) // expecting 3 metric types - def healthMetrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - healthMetrics.start() + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() when: - healthMetrics.onInjectionSucceed() - healthMetrics.onInjectionFailed() - healthMetrics.onInjectionSkipped() + metrics.onInjectionSucceed() + metrics.onInjectionFailed() + metrics.onInjectionSkipped() latch.await(5, TimeUnit.SECONDS) then: @@ -86,23 +86,59 @@ class RumInjectorMetricsTest extends Specification { 0 * _ cleanup: - healthMetrics.close() + metrics.close() } - def "test summary"() { + def "test that flushing only reports non-zero deltas"() { + setup: + def latch = new CountDownLatch(1) // expecting only 1 metric call (non-zero delta) + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() + when: - healthMetrics.onInjectionSucceed() - healthMetrics.onInjectionFailed() - healthMetrics.onInjectionSkipped() - def summary = healthMetrics.summary() + metrics.onInjectionSucceed() + metrics.onInjectionSucceed() + latch.await(5, TimeUnit.SECONDS) then: - summary.contains("injectionSucceed=1") - summary.contains("injectionFailed=1") + 1 * statsD.count('rum.injection.succeed', 2, _) + // should not be called since they have delta of 0 + 0 * statsD.count('rum.injection.failed', _, _) + 0 * statsD.count('rum.injection.skipped', _, _) + 0 * _ + + cleanup: + metrics.close() + } + + def "test summary with multiple events"() { + when: + metrics.onInjectionSucceed() + metrics.onInjectionFailed() + metrics.onInjectionSucceed() + metrics.onInjectionFailed() + metrics.onInjectionSucceed() + metrics.onInjectionSkipped() + def summary = metrics.summary() + + then: + summary.contains("injectionSucceed=3") + summary.contains("injectionFailed=2") summary.contains("injectionSkipped=1") 0 * _ } + def "test metrics start at zero"() { + when: + def summary = metrics.summary() + + then: + summary.contains("injectionSucceed=0") + summary.contains("injectionFailed=0") + summary.contains("injectionSkipped=0") + 0 * _ + } + // taken from HealthMetricsTest private static class Latched implements StatsDClient { final StatsDClient delegate diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index ce09ad814e4..9b7b200e345 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -68,56 +68,110 @@ class RumInjectorTest extends DDSpecification { void 'set telemetry collector'() { setup: - def mockTelemetryCollector = mock(RumTelemetryCollector) + def telemetryCollector = mock(RumTelemetryCollector) when: - RumInjector.setTelemetryCollector(mockTelemetryCollector) - def telemetryCollector = RumInjector.getTelemetryCollector() + RumInjector.setTelemetryCollector(telemetryCollector) then: - telemetryCollector == mockTelemetryCollector + RumInjector.getTelemetryCollector() == telemetryCollector + + cleanup: + RumInjector.setTelemetryCollector(RumTelemetryCollector.NO_OP) } void 'return NO_OP when telemetry collector is not set'() { when: RumInjector.setTelemetryCollector(null) - def telemetryCollector = RumInjector.getTelemetryCollector() then: - telemetryCollector == RumTelemetryCollector.NO_OP + RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP + + cleanup: + RumInjector.setTelemetryCollector(RumTelemetryCollector.NO_OP) } void 'enable telemetry with StatsDClient'() { - setup: - def mockStatsDClient = mock(datadog.trace.api.StatsDClient) - RumInjector.enableTelemetry(mockStatsDClient) - when: - def telemetryCollector = RumInjector.getTelemetryCollector() + RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) then: - telemetryCollector instanceof datadog.trace.api.rum.RumInjectorMetrics + RumInjector.getTelemetryCollector() instanceof datadog.trace.api.rum.RumInjectorMetrics + + cleanup: + RumInjector.shutdownTelemetry() } void 'enabling telemetry with a null StatsDClient sets the telemetry collector to NO_OP'() { when: RumInjector.enableTelemetry(null) - def telemetryCollector = RumInjector.getTelemetryCollector() then: - telemetryCollector == RumTelemetryCollector.NO_OP + RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP + + cleanup: + RumInjector.shutdownTelemetry() } void 'shutdown telemetry'() { setup: - def mockStatsDClient = mock(datadog.trace.api.StatsDClient) - RumInjector.enableTelemetry(mockStatsDClient) + RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) when: RumInjector.shutdownTelemetry() + + then: + RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP + } + + void 'telemetry integration works end-to-end'() { + when: + // simulate CoreTracer enabling telemetry + RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) + + // simulate reporting successful injection def telemetryCollector = RumInjector.getTelemetryCollector() + telemetryCollector.onInjectionSucceed() + telemetryCollector.onInjectionSucceed() + telemetryCollector.onInjectionFailed() + + // verify metrics are collected + def summary = telemetryCollector.summary() then: - telemetryCollector == RumTelemetryCollector.NO_OP + summary.contains("injectionSucceed=2") + summary.contains("injectionFailed=1") + summary.contains("injectionSkipped=0") + + cleanup: + RumInjector.shutdownTelemetry() + } + + void 'concurrent telemetry calls are thread-safe'() { + setup: + RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) + def telemetryCollector = RumInjector.getTelemetryCollector() + def threads = [] + + when: + // simulate multiple threads calling telemetry methods + (1..50).each { i -> + threads << Thread.start { + telemetryCollector.onInjectionSucceed() + telemetryCollector.onInjectionFailed() + telemetryCollector.onInjectionSkipped() + } + } + threads*.join() + + def summary = telemetryCollector.summary() + + then: + summary.contains("injectionSucceed=50") + summary.contains("injectionFailed=50") + summary.contains("injectionSkipped=50") + + cleanup: + RumInjector.shutdownTelemetry() } } From 3fd498d5e84871d891d072776d0c9d07069b30df Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 1 Aug 2025 16:55:34 -0400 Subject: [PATCH 09/31] Get and test metrics for injection failures and skips --- .../RumHttpServletResponseWrapper.java | 44 ++++++---- .../RumHttpServletResponseWrapperTest.groovy | 85 +++++++++++++++++++ .../RumHttpServletResponseWrapper.java | 44 ++++++---- .../RumHttpServletResponseWrapperTest.groovy | 85 +++++++++++++++++++ 4 files changed, 226 insertions(+), 32 deletions(-) create mode 100644 dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy create mode 100644 dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index c73065a3894..851abdc2a16 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -45,18 +45,24 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { + RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getOutputStream(); } - String encoding = getCharacterEncoding(); - if (encoding == null) { - encoding = Charset.defaultCharset().name(); + try { + String encoding = getCharacterEncoding(); + if (encoding == null) { + encoding = Charset.defaultCharset().name(); + } + outputStream = + new WrappedServletOutputStream( + super.getOutputStream(), + rumInjector.getMarkerBytes(encoding), + rumInjector.getSnippetBytes(encoding), + this::onInjected); + } catch (Exception e) { + RumInjector.getTelemetryCollector().onInjectionFailed(); + throw e; } - outputStream = - new WrappedServletOutputStream( - super.getOutputStream(), - rumInjector.getMarkerBytes(encoding), - rumInjector.getSnippetBytes(encoding), - this::onInjected); return outputStream; } @@ -67,15 +73,21 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { + RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getWriter(); } - wrappedPipeWriter = - new InjectingPipeWriter( - super.getWriter(), - rumInjector.getMarkerChars(), - rumInjector.getSnippetChars(), - this::onInjected); - printWriter = new PrintWriter(wrappedPipeWriter); + try { + wrappedPipeWriter = + new InjectingPipeWriter( + super.getWriter(), + rumInjector.getMarkerChars(), + rumInjector.getSnippetChars(), + this::onInjected); + printWriter = new PrintWriter(wrappedPipeWriter); + } catch (Exception e) { + RumInjector.getTelemetryCollector().onInjectionFailed(); + throw e; + } return printWriter; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy new file mode 100644 index 00000000000..ca42b0fdda7 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -0,0 +1,85 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.rum.RumInjector +import datadog.trace.api.rum.RumTelemetryCollector +import datadog.trace.instrumentation.servlet3.RumHttpServletResponseWrapper +import spock.lang.Subject + +import javax.servlet.http.HttpServletResponse + +class RumHttpServletResponseWrapperTest extends AgentTestRunner { + + def mockResponse = Mock(HttpServletResponse) + def mockTelemetryCollector = Mock(RumTelemetryCollector) + + @Subject + RumHttpServletResponseWrapper wrapper + + void setup() { + wrapper = new RumHttpServletResponseWrapper(mockResponse) + RumInjector.setTelemetryCollector(mockTelemetryCollector) + } + + void cleanup() { + RumInjector.setTelemetryCollector(RumTelemetryCollector.NO_OP) + } + + void 'onInjected calls telemetry collector onInjectionSucceed'() { + when: + wrapper.onInjected() + + then: + 1 * mockTelemetryCollector.onInjectionSucceed() + } + + void 'getOutputStream with non-HTML content reports skipped'() { + setup: + wrapper.setContentType("text/plain") + + when: + wrapper.getOutputStream() + + then: + 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockResponse.getOutputStream() + } + + void 'getWriter with non-HTML content reports skipped'() { + setup: + wrapper.setContentType("text/plain") + + when: + wrapper.getWriter() + + then: + 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockResponse.getWriter() + } + + void 'getOutputStream exception reports failure'() { + setup: + wrapper.setContentType("text/html") + mockResponse.getOutputStream() >> { throw new IOException("stream error") } + + when: + try { + wrapper.getOutputStream() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed() + } + + void 'getWriter exception reports failure'() { + setup: + wrapper.setContentType("text/html") + mockResponse.getWriter() >> { throw new IOException("writer error") } + + when: + try { + wrapper.getWriter() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed() + } +} diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 1f37f9df4ad..94f2b7e5403 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -27,18 +27,24 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { + RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getOutputStream(); } - String encoding = getCharacterEncoding(); - if (encoding == null) { - encoding = Charset.defaultCharset().name(); + try { + String encoding = getCharacterEncoding(); + if (encoding == null) { + encoding = Charset.defaultCharset().name(); + } + outputStream = + new WrappedServletOutputStream( + super.getOutputStream(), + rumInjector.getMarkerBytes(encoding), + rumInjector.getSnippetBytes(encoding), + this::onInjected); + } catch (Exception e) { + RumInjector.getTelemetryCollector().onInjectionFailed(); + throw e; } - outputStream = - new WrappedServletOutputStream( - super.getOutputStream(), - rumInjector.getMarkerBytes(encoding), - rumInjector.getSnippetBytes(encoding), - this::onInjected); return outputStream; } @@ -48,15 +54,21 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { + RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getWriter(); } - wrappedPipeWriter = - new InjectingPipeWriter( - super.getWriter(), - rumInjector.getMarkerChars(), - rumInjector.getSnippetChars(), - this::onInjected); - printWriter = new PrintWriter(wrappedPipeWriter); + try { + wrappedPipeWriter = + new InjectingPipeWriter( + super.getWriter(), + rumInjector.getMarkerChars(), + rumInjector.getSnippetChars(), + this::onInjected); + printWriter = new PrintWriter(wrappedPipeWriter); + } catch (Exception e) { + RumInjector.getTelemetryCollector().onInjectionFailed(); + throw e; + } return printWriter; } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy new file mode 100644 index 00000000000..9e35ec0b4e4 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -0,0 +1,85 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.rum.RumInjector +import datadog.trace.api.rum.RumTelemetryCollector +import datadog.trace.instrumentation.servlet5.RumHttpServletResponseWrapper +import spock.lang.Subject + +import jakarta.servlet.http.HttpServletResponse + +class RumHttpServletResponseWrapperTest extends AgentTestRunner { + + def mockResponse = Mock(HttpServletResponse) + def mockTelemetryCollector = Mock(RumTelemetryCollector) + + @Subject + RumHttpServletResponseWrapper wrapper + + void setup() { + wrapper = new RumHttpServletResponseWrapper(mockResponse) + RumInjector.setTelemetryCollector(mockTelemetryCollector) + } + + void cleanup() { + RumInjector.setTelemetryCollector(RumTelemetryCollector.NO_OP) + } + + void 'onInjected calls telemetry collector onInjectionSucceed'() { + when: + wrapper.onInjected() + + then: + 1 * mockTelemetryCollector.onInjectionSucceed() + } + + void 'getOutputStream with non-HTML content reports skipped'() { + setup: + wrapper.setContentType("text/plain") + + when: + wrapper.getOutputStream() + + then: + 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockResponse.getOutputStream() + } + + void 'getWriter with non-HTML content reports skipped'() { + setup: + wrapper.setContentType("text/plain") + + when: + wrapper.getWriter() + + then: + 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockResponse.getWriter() + } + + void 'getOutputStream exception reports failure'() { + setup: + wrapper.setContentType("text/html") + mockResponse.getOutputStream() >> { throw new IOException("stream error") } + + when: + try { + wrapper.getOutputStream() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed() + } + + void 'getWriter exception reports failure'() { + setup: + wrapper.setContentType("text/html") + mockResponse.getWriter() >> { throw new IOException("writer error") } + + when: + try { + wrapper.getWriter() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed() + } +} From 14c338bdf8c186a9254ee6bc84f5ef773bb1a141 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 1 Aug 2025 18:07:08 -0400 Subject: [PATCH 10/31] Add Content Security Policy and HTTP response size telemetry --- .../RumHttpServletResponseWrapper.java | 32 +++++++++++ .../RumHttpServletResponseWrapperTest.groovy | 53 +++++++++++++++++++ .../RumHttpServletResponseWrapper.java | 32 +++++++++++ .../RumHttpServletResponseWrapperTest.groovy | 53 +++++++++++++++++++ .../trace/api/rum/RumInjectorMetrics.java | 23 +++++++- .../trace/api/rum/RumTelemetryCollector.java | 12 +++++ .../api/rum/RumInjectorMetricsTest.groovy | 40 +++++++++++++- .../trace/api/rum/RumInjectorTest.groovy | 24 +++++++++ 8 files changed, 266 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index 851abdc2a16..dcd16f39100 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -92,8 +92,40 @@ public PrintWriter getWriter() throws IOException { return printWriter; } + @Override + public void setHeader(String name, String value) { + if (name != null) { + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + } else if (lowerName.equals("content-length") && value != null) { + try { + long contentLength = Long.parseLong(value); + RumInjector.getTelemetryCollector().onInjectionResponseSize(contentLength); + } catch (NumberFormatException ignored) { + // ignore? + } + } + } + super.setHeader(name, value); + } + + @Override + public void addHeader(String name, String value) { + if (name != null) { + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + } + } + super.addHeader(name, value); + } + @Override public void setContentLength(int len) { + if (len >= 0) { + RumInjector.getTelemetryCollector().onInjectionResponseSize(len); + } // don't set it since we don't know if we will inject if (!shouldInject) { super.setContentLength(len); diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index ca42b0fdda7..6f5648c0ece 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -82,4 +82,57 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { then: 1 * mockTelemetryCollector.onInjectionFailed() } + + void 'setHeader with Content-Security-Policy reports CSP detected'() { + when: + wrapper.setHeader("Content-Security-Policy", "test") + + then: + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.setHeader("Content-Security-Policy", "test") + } + + void 'addHeader with Content-Security-Policy-Report-Only reports CSP detected'() { + when: + wrapper.addHeader("Content-Security-Policy-Report-Only", "test") + + then: + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.addHeader("Content-Security-Policy-Report-Only", "test") + } + + void 'setHeader with non-CSP header does not report CSP detected'() { + when: + wrapper.setHeader("X-Content-Security-Policy", "test") + + then: + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.setHeader("X-Content-Security-Policy", "test") + } + + void 'addHeader with non-CSP header does not report CSP detected'() { + when: + wrapper.addHeader("X-Content-Security-Policy", "test") + + then: + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") + } + + void 'setHeader with Content-Length reports response size'() { + when: + wrapper.setHeader("Content-Length", "1024") + + then: + 1 * mockTelemetryCollector.onInjectionResponseSize(1024) + 1 * mockResponse.setHeader("Content-Length", "1024") + } + + void 'setContentLength method reports response size'() { + when: + wrapper.setContentLength(1024) + + then: + 1 * mockTelemetryCollector.onInjectionResponseSize(1024) + } } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 94f2b7e5403..77692c751c8 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -73,8 +73,40 @@ public PrintWriter getWriter() throws IOException { return printWriter; } + @Override + public void setHeader(String name, String value) { + if (name != null) { + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + } else if (lowerName.equals("content-length") && value != null) { + try { + long contentLength = Long.parseLong(value); + RumInjector.getTelemetryCollector().onInjectionResponseSize(contentLength); + } catch (NumberFormatException ignored) { + // ignore? + } + } + } + super.setHeader(name, value); + } + + @Override + public void addHeader(String name, String value) { + if (name != null) { + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + } + } + super.addHeader(name, value); + } + @Override public void setContentLength(int len) { + if (len >= 0) { + RumInjector.getTelemetryCollector().onInjectionResponseSize(len); + } // don't set it since we don't know if we will inject if (!shouldInject) { super.setContentLength(len); diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 9e35ec0b4e4..26e6b34e0c8 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -82,4 +82,57 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { then: 1 * mockTelemetryCollector.onInjectionFailed() } + + void 'setHeader with Content-Security-Policy reports CSP detected'() { + when: + wrapper.setHeader("Content-Security-Policy", "test") + + then: + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.setHeader("Content-Security-Policy", "test") + } + + void 'addHeader with Content-Security-Policy-Report-Only reports CSP detected'() { + when: + wrapper.addHeader("Content-Security-Policy-Report-Only", "test") + + then: + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.addHeader("Content-Security-Policy-Report-Only", "test") + } + + void 'setHeader with non-CSP header does not report CSP detected'() { + when: + wrapper.setHeader("X-Content-Security-Policy", "test") + + then: + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.setHeader("X-Content-Security-Policy", "test") + } + + void 'addHeader with non-CSP header does not report CSP detected'() { + when: + wrapper.addHeader("X-Content-Security-Policy", "test") + + then: + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") + } + + void 'setHeader with Content-Length reports response size'() { + when: + wrapper.setHeader("Content-Length", "1024") + + then: + 1 * mockTelemetryCollector.onInjectionResponseSize(1024) + 1 * mockResponse.setHeader("Content-Length", "1024") + } + + void 'setContentLength method reports response size'() { + when: + wrapper.setContentLength(1024) + + then: + 1 * mockTelemetryCollector.onInjectionResponseSize(1024) + } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 38a622512c3..f7f66c33dad 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -23,6 +23,7 @@ public class RumInjectorMetrics implements RumTelemetryCollector { private final AtomicLong injectionSucceed = new AtomicLong(); private final AtomicLong injectionFailed = new AtomicLong(); private final AtomicLong injectionSkipped = new AtomicLong(); + private final AtomicLong contentSecurityPolicyDetected = new AtomicLong(); private final StatsDClient statsd; private final long interval; @@ -61,6 +62,17 @@ public void onInjectionSkipped() { injectionSkipped.incrementAndGet(); } + @Override + public void onContentSecurityPolicyDetected() { + contentSecurityPolicyDetected.incrementAndGet(); + } + + @Override + public void onInjectionResponseSize(long bytes) { + // report distribution metric immediately + statsd.distribution("rum.injection.response.bytes", bytes, NO_TAGS); + } + public void close() { if (null != cancellation) { cancellation.cancel(); @@ -73,12 +85,14 @@ public String summary() { + "\ninjectionFailed=" + injectionFailed.get() + "\ninjectionSkipped=" - + injectionSkipped.get(); + + injectionSkipped.get() + + "\ncontentSecurityPolicyDetected=" + + contentSecurityPolicyDetected.get(); } private static class Flush implements AgentTaskScheduler.Task { - private final long[] previousCounts = new long[3]; // one per counter + private final long[] previousCounts = new long[4]; // one per counter private int countIndex; @Override @@ -88,6 +102,11 @@ public void run(RumInjectorMetrics target) { reportIfChanged(target.statsd, "rum.injection.succeed", target.injectionSucceed, NO_TAGS); reportIfChanged(target.statsd, "rum.injection.failed", target.injectionFailed, NO_TAGS); reportIfChanged(target.statsd, "rum.injection.skipped", target.injectionSkipped, NO_TAGS); + reportIfChanged( + target.statsd, + "rum.injection.content_security_policy", + target.contentSecurityPolicyDetected, + NO_TAGS); } catch (ArrayIndexOutOfBoundsException e) { log.warn( "previousCounts array needs resizing to at least {}, was {}", diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 5806f3f157e..33aaad038de 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -18,6 +18,12 @@ public void onInjectionFailed() {} @Override public void onInjectionSkipped() {} + @Override + public void onContentSecurityPolicyDetected() {} + + @Override + public void onInjectionResponseSize(long bytes) {} + @Override public void close() {} @@ -38,6 +44,12 @@ default void start() {} // call when RUM injection is skipped void onInjectionSkipped(); + // call when a Content Security Policy header is detected + void onContentSecurityPolicyDetected(); + + // call to get the response size (in bytes) before RUM injection + void onInjectionResponseSize(long bytes); + default void close() {} // human-readable summary of the current health metrics diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index baae9a5a8d3..7fcb43266e4 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -67,9 +67,40 @@ class RumInjectorMetricsTest extends Specification { metrics.close() } + def "test onContentSecurityPolicyDetected"() { + setup: + def latch = new CountDownLatch(1) + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() + + when: + metrics.onContentSecurityPolicyDetected() + latch.await(5, TimeUnit.SECONDS) + + then: + 1 * statsD.count('rum.injection.content_security_policy', 1, _) + 0 * _ + + cleanup: + metrics.close() + } + + def "test onInjectionResponseSize with multiple sizes"() { + when: + metrics.onInjectionResponseSize(512) + metrics.onInjectionResponseSize(2048) + metrics.onInjectionResponseSize(256) + + then: + 1 * statsD.distribution('rum.injection.response.bytes', 512, _) + 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) + 1 * statsD.distribution('rum.injection.response.bytes', 256, _) + 0 * _ + } + def "test flushing multiple events"() { setup: - def latch = new CountDownLatch(3) // expecting 3 metric types + def latch = new CountDownLatch(4) // expecting 4 metric types def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) metrics.start() @@ -77,12 +108,14 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionSucceed() metrics.onInjectionFailed() metrics.onInjectionSkipped() + metrics.onContentSecurityPolicyDetected() latch.await(5, TimeUnit.SECONDS) then: 1 * statsD.count('rum.injection.succeed', 1, _) 1 * statsD.count('rum.injection.failed', 1, _) 1 * statsD.count('rum.injection.skipped', 1, _) + 1 * statsD.count('rum.injection.content_security_policy', 1, _) 0 * _ cleanup: @@ -105,6 +138,7 @@ class RumInjectorMetricsTest extends Specification { // should not be called since they have delta of 0 0 * statsD.count('rum.injection.failed', _, _) 0 * statsD.count('rum.injection.skipped', _, _) + 0 * statsD.count('rum.injection.content_security_policy', _, _) 0 * _ cleanup: @@ -119,12 +153,15 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionFailed() metrics.onInjectionSucceed() metrics.onInjectionSkipped() + metrics.onContentSecurityPolicyDetected() + metrics.onContentSecurityPolicyDetected() def summary = metrics.summary() then: summary.contains("injectionSucceed=3") summary.contains("injectionFailed=2") summary.contains("injectionSkipped=1") + summary.contains("contentSecurityPolicyDetected=2") 0 * _ } @@ -136,6 +173,7 @@ class RumInjectorMetricsTest extends Specification { summary.contains("injectionSucceed=0") summary.contains("injectionFailed=0") summary.contains("injectionSkipped=0") + summary.contains("contentSecurityPolicyDetected=0") 0 * _ } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index 9b7b200e345..39185f5341b 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -134,6 +134,7 @@ class RumInjectorTest extends DDSpecification { telemetryCollector.onInjectionSucceed() telemetryCollector.onInjectionSucceed() telemetryCollector.onInjectionFailed() + telemetryCollector.onContentSecurityPolicyDetected() // verify metrics are collected def summary = telemetryCollector.summary() @@ -142,6 +143,27 @@ class RumInjectorTest extends DDSpecification { summary.contains("injectionSucceed=2") summary.contains("injectionFailed=1") summary.contains("injectionSkipped=0") + summary.contains("contentSecurityPolicyDetected=1") + + cleanup: + RumInjector.shutdownTelemetry() + } + + void 'response size telemetry does not throw an exception'() { + setup: + def mockStatsDClient = mock(datadog.trace.api.StatsDClient) + + when: + RumInjector.enableTelemetry(mockStatsDClient) + + def telemetryCollector = RumInjector.getTelemetryCollector() + telemetryCollector.onInjectionResponseSize(512) + telemetryCollector.onInjectionResponseSize(2048) + telemetryCollector.onInjectionResponseSize(256) + + then: + // response sizes are reported immediately as distribution metrics + noExceptionThrown() cleanup: RumInjector.shutdownTelemetry() @@ -160,6 +182,7 @@ class RumInjectorTest extends DDSpecification { telemetryCollector.onInjectionSucceed() telemetryCollector.onInjectionFailed() telemetryCollector.onInjectionSkipped() + telemetryCollector.onContentSecurityPolicyDetected() } } threads*.join() @@ -170,6 +193,7 @@ class RumInjectorTest extends DDSpecification { summary.contains("injectionSucceed=50") summary.contains("injectionFailed=50") summary.contains("injectionSkipped=50") + summary.contains("contentSecurityPolicyDetected=50") cleanup: RumInjector.shutdownTelemetry() From c5b53895c29c0ca9a9cddb477cc71386c0922096 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 1 Aug 2025 18:45:43 -0400 Subject: [PATCH 11/31] Add injection duration telemetry --- .../RumHttpServletResponseWrapper.java | 19 ++++++++++++++++++ .../RumHttpServletResponseWrapperTest.groovy | 2 ++ .../RumHttpServletResponseWrapper.java | 19 ++++++++++++++++++ .../RumHttpServletResponseWrapperTest.groovy | 2 ++ .../trace/api/rum/RumInjectorMetrics.java | 6 ++++++ .../trace/api/rum/RumTelemetryCollector.java | 8 +++++++- .../api/rum/RumInjectorMetricsTest.groovy | 17 ++++++++++++++-- .../trace/api/rum/RumInjectorTest.groovy | 20 +++++++++++++++++++ 8 files changed, 90 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index dcd16f39100..034f7d184fb 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -18,6 +18,7 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private PrintWriter printWriter; private InjectingPipeWriter wrappedPipeWriter; private boolean shouldInject = true; + private long injectionStartTime = -1; private static final MethodHandle SET_CONTENT_LENGTH_LONG = getMh("setContentLengthLong"); @@ -48,6 +49,10 @@ public ServletOutputStream getOutputStream() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getOutputStream(); } + // start timing injection + if (injectionStartTime == -1) { + injectionStartTime = System.nanoTime(); + } try { String encoding = getCharacterEncoding(); if (encoding == null) { @@ -76,6 +81,10 @@ public PrintWriter getWriter() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getWriter(); } + // start timing injection + if (injectionStartTime == -1) { + injectionStartTime = System.nanoTime(); + } try { wrappedPipeWriter = new InjectingPipeWriter( @@ -149,6 +158,7 @@ public void reset() { this.wrappedPipeWriter = null; this.printWriter = null; this.shouldInject = false; + this.injectionStartTime = -1; super.reset(); } @@ -162,6 +172,15 @@ public void resetBuffer() { public void onInjected() { RumInjector.getTelemetryCollector().onInjectionSucceed(); + + // report injection time + if (injectionStartTime != -1) { + long nanoseconds = System.nanoTime() - injectionStartTime; + long milliseconds = nanoseconds / 1_000_000L; + RumInjector.getTelemetryCollector().onInjectionTime(milliseconds); + injectionStartTime = -1; + } + try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 6f5648c0ece..0bfcbac4843 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -135,4 +135,6 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { then: 1 * mockTelemetryCollector.onInjectionResponseSize(1024) } + + // test injection timing? } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 77692c751c8..9c9bd560564 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -15,6 +15,7 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private InjectingPipeWriter wrappedPipeWriter; private PrintWriter printWriter; private boolean shouldInject = true; + private long injectionStartTime = -1; public RumHttpServletResponseWrapper(HttpServletResponse response) { super(response); @@ -30,6 +31,10 @@ public ServletOutputStream getOutputStream() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getOutputStream(); } + // start timing injection + if (injectionStartTime == -1) { + injectionStartTime = System.nanoTime(); + } try { String encoding = getCharacterEncoding(); if (encoding == null) { @@ -57,6 +62,10 @@ public PrintWriter getWriter() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(); return super.getWriter(); } + // start timing installation + if (injectionStartTime == -1) { + injectionStartTime = System.nanoTime(); + } try { wrappedPipeWriter = new InjectingPipeWriter( @@ -126,6 +135,7 @@ public void reset() { this.wrappedPipeWriter = null; this.printWriter = null; this.shouldInject = false; + this.injectionStartTime = -1; super.reset(); } @@ -139,6 +149,15 @@ public void resetBuffer() { public void onInjected() { RumInjector.getTelemetryCollector().onInjectionSucceed(); + + // report injection time + if (injectionStartTime != -1) { + long nanoseconds = System.nanoTime() - injectionStartTime; + long milliseconds = nanoseconds / 1_000_000L; + RumInjector.getTelemetryCollector().onInjectionTime(milliseconds); + injectionStartTime = -1; + } + try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 26e6b34e0c8..e6e468ffb14 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -135,4 +135,6 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { then: 1 * mockTelemetryCollector.onInjectionResponseSize(1024) } + + // test injection timing? } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index f7f66c33dad..4180af84fb2 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -73,6 +73,12 @@ public void onInjectionResponseSize(long bytes) { statsd.distribution("rum.injection.response.bytes", bytes, NO_TAGS); } + @Override + public void onInjectionTime(long milliseconds) { + // report distribution metric immediately + statsd.distribution("rum.injection.ms", milliseconds, NO_TAGS); + } + public void close() { if (null != cancellation) { cancellation.cancel(); diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 33aaad038de..2cd61596d50 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -24,6 +24,9 @@ public void onContentSecurityPolicyDetected() {} @Override public void onInjectionResponseSize(long bytes) {} + @Override + public void onInjectionTime(long milliseconds) {} + @Override public void close() {} @@ -47,9 +50,12 @@ default void start() {} // call when a Content Security Policy header is detected void onContentSecurityPolicyDetected(); - // call to get the response size (in bytes) before RUM injection + // call to get the response size before RUM injection void onInjectionResponseSize(long bytes); + // call to report the time it takes to inject the RUM SDK + void onInjectionTime(long milliseconds); + default void close() {} // human-readable summary of the current health metrics diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 7fcb43266e4..59033baa5ae 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -87,14 +87,27 @@ class RumInjectorMetricsTest extends Specification { def "test onInjectionResponseSize with multiple sizes"() { when: + metrics.onInjectionResponseSize(256) metrics.onInjectionResponseSize(512) metrics.onInjectionResponseSize(2048) - metrics.onInjectionResponseSize(256) then: + 1 * statsD.distribution('rum.injection.response.bytes', 256, _) 1 * statsD.distribution('rum.injection.response.bytes', 512, _) 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) - 1 * statsD.distribution('rum.injection.response.bytes', 256, _) + 0 * _ + } + + def "test onInjectionTime with multiple durations"() { + when: + metrics.onInjectionTime(5L) + metrics.onInjectionTime(10L) + metrics.onInjectionTime(25L) + + then: + 1 * statsD.distribution('rum.injection.ms', 5L, _) + 1 * statsD.distribution('rum.injection.ms', 10L, _) + 1 * statsD.distribution('rum.injection.ms', 25L, _) 0 * _ } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index 39185f5341b..e71791ce6df 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -169,6 +169,26 @@ class RumInjectorTest extends DDSpecification { RumInjector.shutdownTelemetry() } + void 'injection time telemetry does not throw an exception'() { + setup: + def mockStatsDClient = mock(datadog.trace.api.StatsDClient) + + when: + RumInjector.enableTelemetry(mockStatsDClient) + + def telemetryCollector = RumInjector.getTelemetryCollector() + telemetryCollector.onInjectionTime(5L) + telemetryCollector.onInjectionTime(10L) + telemetryCollector.onInjectionTime(20L) + + then: + // injection times are reported immediately as distribution metrics + noExceptionThrown() + + cleanup: + RumInjector.shutdownTelemetry() + } + void 'concurrent telemetry calls are thread-safe'() { setup: RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) From feb40be7c8f59b0440f64d092ef722035050a780 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 1 Aug 2025 23:07:55 -0400 Subject: [PATCH 12/31] Fix some things --- .../RumHttpServletResponseWrapperTest.groovy | 6 +- .../RumHttpServletResponseWrapperTest.groovy | 6 +- .../api/rum/RumInjectorMetricsTest.groovy | 59 ++++++++++--------- .../trace/api/rum/RumInjectorTest.groovy | 20 +++---- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 0bfcbac4843..28cae6d7181 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -92,13 +92,13 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.setHeader("Content-Security-Policy", "test") } - void 'addHeader with Content-Security-Policy-Report-Only reports CSP detected'() { + void 'addHeader with Content-Security-Policy reports CSP detected'() { when: - wrapper.addHeader("Content-Security-Policy-Report-Only", "test") + wrapper.addHeader("Content-Security-Policy", "test") then: 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() - 1 * mockResponse.addHeader("Content-Security-Policy-Report-Only", "test") + 1 * mockResponse.addHeader("Content-Security-Policy", "test") } void 'setHeader with non-CSP header does not report CSP detected'() { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index e6e468ffb14..8431bff3269 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -92,13 +92,13 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.setHeader("Content-Security-Policy", "test") } - void 'addHeader with Content-Security-Policy-Report-Only reports CSP detected'() { + void 'addHeader with Content-Security-Policy reports CSP detected'() { when: - wrapper.addHeader("Content-Security-Policy-Report-Only", "test") + wrapper.addHeader("Content-Security-Policy", "test") then: 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() - 1 * mockResponse.addHeader("Content-Security-Policy-Report-Only", "test") + 1 * mockResponse.addHeader("Content-Security-Policy", "test") } void 'setHeader with non-CSP header does not report CSP detected'() { diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 59033baa5ae..d737bd59d02 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -85,33 +85,7 @@ class RumInjectorMetricsTest extends Specification { metrics.close() } - def "test onInjectionResponseSize with multiple sizes"() { - when: - metrics.onInjectionResponseSize(256) - metrics.onInjectionResponseSize(512) - metrics.onInjectionResponseSize(2048) - - then: - 1 * statsD.distribution('rum.injection.response.bytes', 256, _) - 1 * statsD.distribution('rum.injection.response.bytes', 512, _) - 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) - 0 * _ - } - - def "test onInjectionTime with multiple durations"() { - when: - metrics.onInjectionTime(5L) - metrics.onInjectionTime(10L) - metrics.onInjectionTime(25L) - - then: - 1 * statsD.distribution('rum.injection.ms', 5L, _) - 1 * statsD.distribution('rum.injection.ms', 10L, _) - 1 * statsD.distribution('rum.injection.ms', 25L, _) - 0 * _ - } - - def "test flushing multiple events"() { + def "test multiple events"() { setup: def latch = new CountDownLatch(4) // expecting 4 metric types def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) @@ -158,8 +132,9 @@ class RumInjectorMetricsTest extends Specification { metrics.close() } - def "test summary with multiple events"() { + def "test summary with multiple events in different order"() { when: + metrics.onContentSecurityPolicyDetected() metrics.onInjectionSucceed() metrics.onInjectionFailed() metrics.onInjectionSucceed() @@ -167,7 +142,6 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionSucceed() metrics.onInjectionSkipped() metrics.onContentSecurityPolicyDetected() - metrics.onContentSecurityPolicyDetected() def summary = metrics.summary() then: @@ -190,6 +164,33 @@ class RumInjectorMetricsTest extends Specification { 0 * _ } + // events below are reported immediately as distribution metrics and do not get summarized + def "test onInjectionResponseSize with multiple sizes"() { + when: + metrics.onInjectionResponseSize(256) + metrics.onInjectionResponseSize(512) + metrics.onInjectionResponseSize(2048) + + then: + 1 * statsD.distribution('rum.injection.response.bytes', 256, _) + 1 * statsD.distribution('rum.injection.response.bytes', 512, _) + 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) + 0 * _ + } + + def "test onInjectionTime with multiple durations"() { + when: + metrics.onInjectionTime(5L) + metrics.onInjectionTime(10L) + metrics.onInjectionTime(25L) + + then: + 1 * statsD.distribution('rum.injection.ms', 5L, _) + 1 * statsD.distribution('rum.injection.ms', 10L, _) + 1 * statsD.distribution('rum.injection.ms', 25L, _) + 0 * _ + } + // taken from HealthMetricsTest private static class Latched implements StatsDClient { final StatsDClient delegate diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index e71791ce6df..6603fb247f4 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -86,9 +86,6 @@ class RumInjectorTest extends DDSpecification { then: RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP - - cleanup: - RumInjector.setTelemetryCollector(RumTelemetryCollector.NO_OP) } void 'enable telemetry with StatsDClient'() { @@ -108,9 +105,6 @@ class RumInjectorTest extends DDSpecification { then: RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP - - cleanup: - RumInjector.shutdownTelemetry() } void 'shutdown telemetry'() { @@ -132,17 +126,19 @@ class RumInjectorTest extends DDSpecification { // simulate reporting successful injection def telemetryCollector = RumInjector.getTelemetryCollector() telemetryCollector.onInjectionSucceed() - telemetryCollector.onInjectionSucceed() telemetryCollector.onInjectionFailed() + telemetryCollector.onInjectionSkipped() telemetryCollector.onContentSecurityPolicyDetected() + telemetryCollector.onInjectionResponseSize(256) + telemetryCollector.onInjectionTime(5L) // verify metrics are collected def summary = telemetryCollector.summary() then: - summary.contains("injectionSucceed=2") + summary.contains("injectionSucceed=1") summary.contains("injectionFailed=1") - summary.contains("injectionSkipped=0") + summary.contains("injectionSkipped=1") summary.contains("contentSecurityPolicyDetected=1") cleanup: @@ -157,9 +153,9 @@ class RumInjectorTest extends DDSpecification { RumInjector.enableTelemetry(mockStatsDClient) def telemetryCollector = RumInjector.getTelemetryCollector() + telemetryCollector.onInjectionResponseSize(256) telemetryCollector.onInjectionResponseSize(512) telemetryCollector.onInjectionResponseSize(2048) - telemetryCollector.onInjectionResponseSize(256) then: // response sizes are reported immediately as distribution metrics @@ -189,7 +185,7 @@ class RumInjectorTest extends DDSpecification { RumInjector.shutdownTelemetry() } - void 'concurrent telemetry calls are thread-safe'() { + void 'concurrent telemetry calls return an accurate summary'() { setup: RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) def telemetryCollector = RumInjector.getTelemetryCollector() @@ -203,6 +199,8 @@ class RumInjectorTest extends DDSpecification { telemetryCollector.onInjectionFailed() telemetryCollector.onInjectionSkipped() telemetryCollector.onContentSecurityPolicyDetected() + telemetryCollector.onInjectionResponseSize(256) + telemetryCollector.onInjectionTime(5L) } } threads*.join() From 23e045535e116e4b0d892d2d5d5f93dab1b23dd0 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Wed, 6 Aug 2025 17:03:45 -0400 Subject: [PATCH 13/31] Fix content-length retrieval and add test for injection timing --- .../buffer/InjectingPipeOutputStream.java | 18 ++++++++++- .../RumHttpServletResponseWrapper.java | 16 ++-------- .../servlet3/WrappedServletOutputStream.java | 11 +++++-- .../RumHttpServletResponseWrapperTest.groovy | 32 +++++++++++++------ .../RumHttpServletResponseWrapper.java | 16 ++-------- .../servlet5/WrappedServletOutputStream.java | 12 +++++-- .../RumHttpServletResponseWrapperTest.groovy | 32 +++++++++++++------ 7 files changed, 86 insertions(+), 51 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index 8f91a8e38cd..72a7c527847 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.util.function.LongConsumer; import javax.annotation.concurrent.NotThreadSafe; /** @@ -23,18 +24,22 @@ public class InjectingPipeOutputStream extends OutputStream { private final Runnable onContentInjected; private final int bulkWriteThreshold; private final OutputStream downstream; + private final LongConsumer onBytesWritten; + private long bytesWritten = 0; /** * @param downstream the delegate output stream * @param marker the marker to find in the stream. Must at least be one byte. * @param contentToInject the content to inject once before the marker if found. * @param onContentInjected callback called when and if the content is injected. + * @param onBytesWritten callback called when stream is closed to report total bytes written. */ public InjectingPipeOutputStream( final OutputStream downstream, final byte[] marker, final byte[] contentToInject, - final Runnable onContentInjected) { + final Runnable onContentInjected, + final LongConsumer onBytesWritten) { this.downstream = downstream; this.marker = marker; this.lookbehind = new byte[marker.length]; @@ -46,11 +51,13 @@ public InjectingPipeOutputStream( this.filter = true; this.contentToInject = contentToInject; this.onContentInjected = onContentInjected; + this.onBytesWritten = onBytesWritten; this.bulkWriteThreshold = marker.length * 2 - 2; } @Override public void write(int b) throws IOException { + bytesWritten++; if (!filter) { if (wasDraining) { // continue draining @@ -85,6 +92,7 @@ public void write(int b) throws IOException { @Override public void write(byte[] array, int off, int len) throws IOException { + bytesWritten += len; if (!filter) { if (wasDraining) { // needs drain @@ -113,6 +121,7 @@ public void write(byte[] array, int off, int len) throws IOException { // we don't have a full match. write everything in a bulk except the lookbehind buffer // sequentially for (int i = off; i < off + marker.length - 1; i++) { + bytesWritten--; // avoid double counting write(array[i]); } drain(); @@ -123,12 +132,14 @@ public void write(byte[] array, int off, int len) throws IOException { downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold); filter = wasFiltering; for (int i = len - marker.length + 1; i < len; i++) { + bytesWritten--; // avoid double counting write(array[i]); } } } else { // use slow path because the length to write is small and within the lookbehind buffer size for (int i = off; i < off + len; i++) { + bytesWritten--; // avoid double counting write(array[i]); } } @@ -185,6 +196,11 @@ public void flush() throws IOException { public void close() throws IOException { try { commit(); + // report the size of the original HTTP response before injecting via callback + if (onBytesWritten != null) { + onBytesWritten.accept(bytesWritten); + } + bytesWritten = 0; } finally { downstream.close(); } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index 034f7d184fb..7226d2884f5 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -104,16 +104,8 @@ public PrintWriter getWriter() throws IOException { @Override public void setHeader(String name, String value) { if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { + if (name.toLowerCase().startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); - } else if (lowerName.equals("content-length") && value != null) { - try { - long contentLength = Long.parseLong(value); - RumInjector.getTelemetryCollector().onInjectionResponseSize(contentLength); - } catch (NumberFormatException ignored) { - // ignore? - } } } super.setHeader(name, value); @@ -122,8 +114,7 @@ public void setHeader(String name, String value) { @Override public void addHeader(String name, String value) { if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { + if (name.toLowerCase().startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); } } @@ -132,9 +123,6 @@ public void addHeader(String name, String value) { @Override public void setContentLength(int len) { - if (len >= 0) { - RumInjector.getTelemetryCollector().onInjectionResponseSize(len); - } // don't set it since we don't know if we will inject if (!shouldInject) { super.setContentLength(len); diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java index 109a55491d8..a539adadfec 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java @@ -4,6 +4,7 @@ import datadog.trace.util.MethodHandles; import java.io.IOException; import java.lang.invoke.MethodHandle; +import java.util.function.LongConsumer; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; @@ -29,8 +30,14 @@ private static void sneakyThrow(Throwable e) throws E { } public WrappedServletOutputStream( - ServletOutputStream delegate, byte[] marker, byte[] contentToInject, Runnable onInjected) { - this.filtered = new InjectingPipeOutputStream(delegate, marker, contentToInject, onInjected); + ServletOutputStream delegate, + byte[] marker, + byte[] contentToInject, + Runnable onInjected, + LongConsumer onBytesWritten) { + this.filtered = + new InjectingPipeOutputStream( + delegate, marker, contentToInject, onInjected, onBytesWritten); this.delegate = delegate; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 28cae6d7181..f0f1fa5c12b 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -119,22 +119,36 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } - void 'setHeader with Content-Length reports response size'() { + void 'response sizes are reported correctly'() { + setup: + def downstream = Mock(java.io.OutputStream) + def marker = "".getBytes("UTF-8") + def contentToInject = "".getBytes("UTF-8") + def onBytesWritten = Mock(java.util.function.LongConsumer) + def stream = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream( + downstream, marker, contentToInject, null, onBytesWritten) + when: - wrapper.setHeader("Content-Length", "1024") + stream.write("test".getBytes("UTF-8")) + stream.write("content".getBytes("UTF-8")) + stream.close() then: - 1 * mockTelemetryCollector.onInjectionResponseSize(1024) - 1 * mockResponse.setHeader("Content-Length", "1024") + 1 * onBytesWritten.accept(11) } - void 'setContentLength method reports response size'() { + void 'injection timing is reported when injection is successful'() { + setup: + wrapper.setContentType("text/html") + when: - wrapper.setContentLength(1024) + try { + wrapper.getOutputStream() // set injectionStartTime + } catch (Exception ignored) {} // expect failure due to improper setup + Thread.sleep(1) // ensure some time passes + wrapper.onInjected() // report timing when injection "is successful" then: - 1 * mockTelemetryCollector.onInjectionResponseSize(1024) + 1 * mockTelemetryCollector.onInjectionTime({ it > 0 }) } - - // test injection timing? } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 9c9bd560564..bf85d2d090d 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -85,16 +85,8 @@ public PrintWriter getWriter() throws IOException { @Override public void setHeader(String name, String value) { if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { + if (name.toLowerCase().startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); - } else if (lowerName.equals("content-length") && value != null) { - try { - long contentLength = Long.parseLong(value); - RumInjector.getTelemetryCollector().onInjectionResponseSize(contentLength); - } catch (NumberFormatException ignored) { - // ignore? - } } } super.setHeader(name, value); @@ -103,8 +95,7 @@ public void setHeader(String name, String value) { @Override public void addHeader(String name, String value) { if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { + if (name.toLowerCase().startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); } } @@ -113,9 +104,6 @@ public void addHeader(String name, String value) { @Override public void setContentLength(int len) { - if (len >= 0) { - RumInjector.getTelemetryCollector().onInjectionResponseSize(len); - } // don't set it since we don't know if we will inject if (!shouldInject) { super.setContentLength(len); diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java index db956377708..d780a4411d6 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java @@ -4,14 +4,22 @@ import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; import java.io.IOException; +import java.io.OutputStream; +import java.util.function.LongConsumer; public class WrappedServletOutputStream extends ServletOutputStream { private final InjectingPipeOutputStream filtered; private final ServletOutputStream delegate; public WrappedServletOutputStream( - ServletOutputStream delegate, byte[] marker, byte[] contentToInject, Runnable onInjected) { - this.filtered = new InjectingPipeOutputStream(delegate, marker, contentToInject, onInjected); + ServletOutputStream delegate, + byte[] marker, + byte[] contentToInject, + Runnable onInjected, + LongConsumer onBytesWritten) { + this.filtered = + new InjectingPipeOutputStream( + delegate, marker, contentToInject, onInjected, onBytesWritten); this.delegate = delegate; } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 8431bff3269..b6ef3a97f7c 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -119,22 +119,36 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } - void 'setHeader with Content-Length reports response size'() { + void 'response sizes are reported correctly'() { + setup: + def downstream = Mock(java.io.OutputStream) + def marker = "".getBytes("UTF-8") + def contentToInject = "".getBytes("UTF-8") + def onBytesWritten = Mock(java.util.function.LongConsumer) + def stream = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream( + downstream, marker, contentToInject, null, onBytesWritten) + when: - wrapper.setHeader("Content-Length", "1024") + stream.write("test".getBytes("UTF-8")) + stream.write("content".getBytes("UTF-8")) + stream.close() then: - 1 * mockTelemetryCollector.onInjectionResponseSize(1024) - 1 * mockResponse.setHeader("Content-Length", "1024") + 1 * onBytesWritten.accept(11) } - void 'setContentLength method reports response size'() { + void 'injection timing is reported when injection is successful'() { + setup: + wrapper.setContentType("text/html") + when: - wrapper.setContentLength(1024) + try { + wrapper.getOutputStream() // set injectionStartTime + } catch (Exception ignored) {} // expect failure due to improper setup + Thread.sleep(1) // ensure some time passes + wrapper.onInjected() // report timing when injection "is successful" then: - 1 * mockTelemetryCollector.onInjectionResponseSize(1024) + 1 * mockTelemetryCollector.onInjectionTime({ it > 0 }) } - - // test injection timing? } From 07db1741c9f121e3964efee770964d62f170ce3c Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 7 Aug 2025 16:22:36 -0400 Subject: [PATCH 14/31] Add injection initialization success telemetry --- .../datadog/trace/api/rum/RumInjector.java | 4 +++ .../trace/api/rum/RumInjectorMetrics.java | 17 +++++++++-- .../trace/api/rum/RumTelemetryCollector.java | 6 ++++ .../api/rum/RumInjectorMetricsTest.groovy | 25 +++++++++++++++- .../trace/api/rum/RumInjectorTest.groovy | 29 +++++++++++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index 6c424b20759..e3639488faf 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -132,6 +132,10 @@ public static void enableTelemetry(datadog.trace.api.StatsDClient statsDClient) RumInjectorMetrics metrics = new RumInjectorMetrics(statsDClient); metrics.start(); telemetryCollector = metrics; + + if (INSTANCE.isEnabled()) { + telemetryCollector.onInitializationSucceed(); + } } else { telemetryCollector = RumTelemetryCollector.NO_OP; } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 4180af84fb2..053ebed21b7 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -24,6 +24,7 @@ public class RumInjectorMetrics implements RumTelemetryCollector { private final AtomicLong injectionFailed = new AtomicLong(); private final AtomicLong injectionSkipped = new AtomicLong(); private final AtomicLong contentSecurityPolicyDetected = new AtomicLong(); + private final AtomicLong initializationSucceed = new AtomicLong(); private final StatsDClient statsd; private final long interval; @@ -62,6 +63,11 @@ public void onInjectionSkipped() { injectionSkipped.incrementAndGet(); } + @Override + public void onInitializationSucceed() { + initializationSucceed.incrementAndGet(); + } + @Override public void onContentSecurityPolicyDetected() { contentSecurityPolicyDetected.incrementAndGet(); @@ -93,12 +99,14 @@ public String summary() { + "\ninjectionSkipped=" + injectionSkipped.get() + "\ncontentSecurityPolicyDetected=" - + contentSecurityPolicyDetected.get(); + + contentSecurityPolicyDetected.get() + + "\ninitializationSucceed=" + + initializationSucceed.get(); } private static class Flush implements AgentTaskScheduler.Task { - private final long[] previousCounts = new long[4]; // one per counter + private final long[] previousCounts = new long[5]; // one per counter private int countIndex; @Override @@ -113,6 +121,11 @@ public void run(RumInjectorMetrics target) { "rum.injection.content_security_policy", target.contentSecurityPolicyDetected, NO_TAGS); + reportIfChanged( + target.statsd, + "rum.injection.initialization.succeed", + target.initializationSucceed, + NO_TAGS); } catch (ArrayIndexOutOfBoundsException e) { log.warn( "previousCounts array needs resizing to at least {}, was {}", diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 2cd61596d50..970783796dd 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -18,6 +18,9 @@ public void onInjectionFailed() {} @Override public void onInjectionSkipped() {} + @Override + public void onInitializationSucceed() {} + @Override public void onContentSecurityPolicyDetected() {} @@ -47,6 +50,9 @@ default void start() {} // call when RUM injection is skipped void onInjectionSkipped(); + // call when RUM injector initialization succeeds + void onInitializationSucceed(); + // call when a Content Security Policy header is detected void onContentSecurityPolicyDetected(); diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index d737bd59d02..c319df14b84 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -85,9 +85,27 @@ class RumInjectorMetricsTest extends Specification { metrics.close() } + def "test onInitializationSucceed"() { + setup: + def latch = new CountDownLatch(1) + def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) + metrics.start() + + when: + metrics.onInitializationSucceed() + latch.await(5, TimeUnit.SECONDS) + + then: + 1 * statsD.count('rum.injection.initialization.succeed', 1, _) + 0 * _ + + cleanup: + metrics.close() + } + def "test multiple events"() { setup: - def latch = new CountDownLatch(4) // expecting 4 metric types + def latch = new CountDownLatch(5) // expecting 5 metric types def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) metrics.start() @@ -96,6 +114,7 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionFailed() metrics.onInjectionSkipped() metrics.onContentSecurityPolicyDetected() + metrics.onInitializationSucceed() latch.await(5, TimeUnit.SECONDS) then: @@ -103,6 +122,7 @@ class RumInjectorMetricsTest extends Specification { 1 * statsD.count('rum.injection.failed', 1, _) 1 * statsD.count('rum.injection.skipped', 1, _) 1 * statsD.count('rum.injection.content_security_policy', 1, _) + 1 * statsD.count('rum.injection.initialization.succeed', 1, _) 0 * _ cleanup: @@ -142,6 +162,7 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionSucceed() metrics.onInjectionSkipped() metrics.onContentSecurityPolicyDetected() + metrics.onInitializationSucceed() def summary = metrics.summary() then: @@ -149,6 +170,7 @@ class RumInjectorMetricsTest extends Specification { summary.contains("injectionFailed=2") summary.contains("injectionSkipped=1") summary.contains("contentSecurityPolicyDetected=2") + summary.contains("initializationSucceed=1") 0 * _ } @@ -161,6 +183,7 @@ class RumInjectorMetricsTest extends Specification { summary.contains("injectionFailed=0") summary.contains("injectionSkipped=0") summary.contains("contentSecurityPolicyDetected=0") + summary.contains("initializationSucceed=0") 0 * _ } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index 6603fb247f4..7a8f5dcc435 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -118,6 +118,20 @@ class RumInjectorTest extends DDSpecification { RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP } + void 'content security policy HTTP response detected'() { + when: + RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) + def telemetryCollector = RumInjector.getTelemetryCollector() + telemetryCollector.onContentSecurityPolicyDetected() + def summary = telemetryCollector.summary() + + then: + summary.contains("contentSecurityPolicyDetected=1") + + cleanup: + RumInjector.shutdownTelemetry() + } + void 'telemetry integration works end-to-end'() { when: // simulate CoreTracer enabling telemetry @@ -140,6 +154,7 @@ class RumInjectorTest extends DDSpecification { summary.contains("injectionFailed=1") summary.contains("injectionSkipped=1") summary.contains("contentSecurityPolicyDetected=1") + summary.contains("initializationSucceed=0") // rum injector not initialized in test environment cleanup: RumInjector.shutdownTelemetry() @@ -216,4 +231,18 @@ class RumInjectorTest extends DDSpecification { cleanup: RumInjector.shutdownTelemetry() } + + void 'initialize rum injector successfully'() { + when: + RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) + def telemetryCollector = RumInjector.getTelemetryCollector() + telemetryCollector.onInitializationSucceed() + def summary = telemetryCollector.summary() + + then: + summary.contains("initializationSucceed=1") + + cleanup: + RumInjector.shutdownTelemetry() + } } From a5352ffab44a2f0e6d740844f655b2125e4740f7 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 8 Aug 2025 12:14:14 -0400 Subject: [PATCH 15/31] Fix CoreTracer compilation with InstrumenterConfig --- dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index abb240a75ae..d68127abf63 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -30,6 +30,7 @@ import datadog.trace.api.DynamicConfig; import datadog.trace.api.EndpointTracker; import datadog.trace.api.IdGenerationStrategy; +import datadog.trace.api.InstrumenterConfig; import datadog.trace.api.StatsDClient; import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; @@ -706,7 +707,7 @@ private CoreTracer( healthMetrics.start(); // Start RUM injector telemetry - if (config.isRumEnabled()) { + if (InstrumenterConfig.get().isRumEnabled()) { RumInjector.enableTelemetry(this.statsDClient); } From 2a1f676e6778528f975563ed91347e8ce732cf47 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 8 Aug 2025 15:20:17 -0400 Subject: [PATCH 16/31] Add tags to all metrics --- .../RumHttpServletResponseWrapper.java | 30 +- .../RumHttpServletResponseWrapperTest.groovy | 40 +- .../RumHttpServletResponseWrapper.java | 30 +- .../servlet5/WrappedServletOutputStream.java | 1 - .../RumHttpServletResponseWrapperTest.groovy | 40 +- .../datadog/trace/api/rum/RumInjector.java | 1 - .../trace/api/rum/RumInjectorMetrics.java | 213 ++++++---- .../trace/api/rum/RumTelemetryCollector.java | 29 +- .../api/rum/RumInjectorMetricsTest.groovy | 397 +++++++----------- .../trace/api/rum/RumInjectorTest.groovy | 40 +- 10 files changed, 415 insertions(+), 406 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index 7226d2884f5..5877262c0c7 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -19,6 +19,7 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private InjectingPipeWriter wrappedPipeWriter; private boolean shouldInject = true; private long injectionStartTime = -1; + private String contentEncoding = "none"; private static final MethodHandle SET_CONTENT_LENGTH_LONG = getMh("setContentLengthLong"); @@ -46,7 +47,7 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(); + RumInjector.getTelemetryCollector().onInjectionSkipped("3"); return super.getOutputStream(); } // start timing injection @@ -63,9 +64,10 @@ public ServletOutputStream getOutputStream() throws IOException { super.getOutputStream(), rumInjector.getMarkerBytes(encoding), rumInjector.getSnippetBytes(encoding), - this::onInjected); + this::onInjected, + bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("3", bytes)); } catch (Exception e) { - RumInjector.getTelemetryCollector().onInjectionFailed(); + RumInjector.getTelemetryCollector().onInjectionFailed("3", contentEncoding); throw e; } @@ -78,7 +80,7 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(); + RumInjector.getTelemetryCollector().onInjectionSkipped("3"); return super.getWriter(); } // start timing injection @@ -94,7 +96,7 @@ public PrintWriter getWriter() throws IOException { this::onInjected); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { - RumInjector.getTelemetryCollector().onInjectionFailed(); + RumInjector.getTelemetryCollector().onInjectionFailed("3", contentEncoding); throw e; } @@ -104,8 +106,11 @@ public PrintWriter getWriter() throws IOException { @Override public void setHeader(String name, String value) { if (name != null) { - if (name.toLowerCase().startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("3"); + } else if (lowerName.equals("content-encoding")) { + this.contentEncoding = value; } } super.setHeader(name, value); @@ -114,8 +119,11 @@ public void setHeader(String name, String value) { @Override public void addHeader(String name, String value) { if (name != null) { - if (name.toLowerCase().startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("3"); + } else if (lowerName.equals("content-encoding")) { + this.contentEncoding = value; } } super.addHeader(name, value); @@ -159,13 +167,13 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.getTelemetryCollector().onInjectionSucceed(); + RumInjector.getTelemetryCollector().onInjectionSucceed("3"); // report injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime(milliseconds); + RumInjector.getTelemetryCollector().onInjectionTime("3", milliseconds); injectionStartTime = -1; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index f0f1fa5c12b..db8ebb5462c 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -28,7 +28,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() then: - 1 * mockTelemetryCollector.onInjectionSucceed() + 1 * mockTelemetryCollector.onInjectionSucceed("3") } void 'getOutputStream with non-HTML content reports skipped'() { @@ -39,7 +39,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getOutputStream() then: - 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockTelemetryCollector.onInjectionSkipped("3") 1 * mockResponse.getOutputStream() } @@ -51,7 +51,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getWriter() then: - 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockTelemetryCollector.onInjectionSkipped("3") 1 * mockResponse.getWriter() } @@ -66,7 +66,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed() + 1 * mockTelemetryCollector.onInjectionFailed("3", "none") } void 'getWriter exception reports failure'() { @@ -80,7 +80,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed() + 1 * mockTelemetryCollector.onInjectionFailed("3", "none") } void 'setHeader with Content-Security-Policy reports CSP detected'() { @@ -88,7 +88,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") 1 * mockResponse.setHeader("Content-Security-Policy", "test") } @@ -97,7 +97,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") 1 * mockResponse.addHeader("Content-Security-Policy", "test") } @@ -119,7 +119,29 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } - void 'response sizes are reported correctly'() { + // Callback is created in the RumHttpServletResponseWrapper and passed to InjectingPipeOutputStream via WrappedServletOutputStream. + // When the stream is closed, the callback is called with the total number of bytes written to the stream. + void 'response sizes are reported to the telemetry collector via the WrappedServletOutputStream callback'() { + setup: + def downstream = Mock(javax.servlet.ServletOutputStream) + def marker = "".getBytes("UTF-8") + def contentToInject = "".getBytes("UTF-8") + def onBytesWritten = { bytes -> + mockTelemetryCollector.onInjectionResponseSize("3", bytes) + } + def wrappedStream = new datadog.trace.instrumentation.servlet3.WrappedServletOutputStream( + downstream, marker, contentToInject, null, onBytesWritten) + + when: + wrappedStream.write("test".getBytes("UTF-8")) + wrappedStream.write("content".getBytes("UTF-8")) + wrappedStream.close() + + then: + 1 * mockTelemetryCollector.onInjectionResponseSize("3", 11) + } + + void 'response sizes are reported by the InjectingPipeOutputStream callback'() { setup: def downstream = Mock(java.io.OutputStream) def marker = "".getBytes("UTF-8") @@ -149,6 +171,6 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() // report timing when injection "is successful" then: - 1 * mockTelemetryCollector.onInjectionTime({ it > 0 }) + 1 * mockTelemetryCollector.onInjectionTime("3", { it >= 0 }) } } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index bf85d2d090d..0f526d9068a 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -16,6 +16,7 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private PrintWriter printWriter; private boolean shouldInject = true; private long injectionStartTime = -1; + private String contentEncoding = "none"; public RumHttpServletResponseWrapper(HttpServletResponse response) { super(response); @@ -28,7 +29,7 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(); + RumInjector.getTelemetryCollector().onInjectionSkipped("5"); return super.getOutputStream(); } // start timing injection @@ -45,9 +46,10 @@ public ServletOutputStream getOutputStream() throws IOException { super.getOutputStream(), rumInjector.getMarkerBytes(encoding), rumInjector.getSnippetBytes(encoding), - this::onInjected); + this::onInjected, + bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("5", bytes)); } catch (Exception e) { - RumInjector.getTelemetryCollector().onInjectionFailed(); + RumInjector.getTelemetryCollector().onInjectionFailed("5", contentEncoding); throw e; } return outputStream; @@ -59,7 +61,7 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(); + RumInjector.getTelemetryCollector().onInjectionSkipped("5"); return super.getWriter(); } // start timing installation @@ -75,7 +77,7 @@ public PrintWriter getWriter() throws IOException { this::onInjected); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { - RumInjector.getTelemetryCollector().onInjectionFailed(); + RumInjector.getTelemetryCollector().onInjectionFailed("5", contentEncoding); throw e; } @@ -85,8 +87,11 @@ public PrintWriter getWriter() throws IOException { @Override public void setHeader(String name, String value) { if (name != null) { - if (name.toLowerCase().startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("5"); + } else if (lowerName.equals("content-encoding")) { + this.contentEncoding = value; } } super.setHeader(name, value); @@ -95,8 +100,11 @@ public void setHeader(String name, String value) { @Override public void addHeader(String name, String value) { if (name != null) { - if (name.toLowerCase().startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(); + String lowerName = name.toLowerCase(); + if (lowerName.startsWith("content-security-policy")) { + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("5"); + } else if (lowerName.equals("content-encoding")) { + this.contentEncoding = value; } } super.addHeader(name, value); @@ -136,13 +144,13 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.getTelemetryCollector().onInjectionSucceed(); + RumInjector.getTelemetryCollector().onInjectionSucceed("5"); // report injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime(milliseconds); + RumInjector.getTelemetryCollector().onInjectionTime("5", milliseconds); injectionStartTime = -1; } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java index d780a4411d6..daf6dcaaafa 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java @@ -4,7 +4,6 @@ import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; import java.io.IOException; -import java.io.OutputStream; import java.util.function.LongConsumer; public class WrappedServletOutputStream extends ServletOutputStream { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index b6ef3a97f7c..43d37e3c271 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -28,7 +28,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() then: - 1 * mockTelemetryCollector.onInjectionSucceed() + 1 * mockTelemetryCollector.onInjectionSucceed("5") } void 'getOutputStream with non-HTML content reports skipped'() { @@ -39,7 +39,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getOutputStream() then: - 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockTelemetryCollector.onInjectionSkipped("5") 1 * mockResponse.getOutputStream() } @@ -51,7 +51,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getWriter() then: - 1 * mockTelemetryCollector.onInjectionSkipped() + 1 * mockTelemetryCollector.onInjectionSkipped("5") 1 * mockResponse.getWriter() } @@ -66,7 +66,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed() + 1 * mockTelemetryCollector.onInjectionFailed("5", "none") } void 'getWriter exception reports failure'() { @@ -80,7 +80,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed() + 1 * mockTelemetryCollector.onInjectionFailed("5", "none") } void 'setHeader with Content-Security-Policy reports CSP detected'() { @@ -88,7 +88,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") 1 * mockResponse.setHeader("Content-Security-Policy", "test") } @@ -97,7 +97,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") 1 * mockResponse.addHeader("Content-Security-Policy", "test") } @@ -119,7 +119,29 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } - void 'response sizes are reported correctly'() { + // Callback is created in the RumHttpServletResponseWrapper and passed to InjectingPipeOutputStream via WrappedServletOutputStream. + // When the stream is closed, the callback is called with the total number of bytes written to the stream. + void 'response sizes are reported to the telemetry collector via the WrappedServletOutputStream callback'() { + setup: + def downstream = Mock(jakarta.servlet.ServletOutputStream) + def marker = "".getBytes("UTF-8") + def contentToInject = "".getBytes("UTF-8") + def onBytesWritten = { bytes -> + mockTelemetryCollector.onInjectionResponseSize("5", bytes) + } + def wrappedStream = new datadog.trace.instrumentation.servlet5.WrappedServletOutputStream( + downstream, marker, contentToInject, null, onBytesWritten) + + when: + wrappedStream.write("test".getBytes("UTF-8")) + wrappedStream.write("content".getBytes("UTF-8")) + wrappedStream.close() + + then: + 1 * mockTelemetryCollector.onInjectionResponseSize("5", 11) + } + + void 'response sizes are reported by the InjectingPipeOutputStream callback'() { setup: def downstream = Mock(java.io.OutputStream) def marker = "".getBytes("UTF-8") @@ -149,6 +171,6 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() // report timing when injection "is successful" then: - 1 * mockTelemetryCollector.onInjectionTime({ it > 0 }) + 1 * mockTelemetryCollector.onInjectionTime("5", { it >= 0 }) } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index e3639488faf..9b0b420508d 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -130,7 +130,6 @@ public byte[] getMarkerBytes(String encoding) { public static void enableTelemetry(datadog.trace.api.StatsDClient statsDClient) { if (statsDClient != null) { RumInjectorMetrics metrics = new RumInjectorMetrics(statsDClient); - metrics.start(); telemetryCollector = metrics; if (INSTANCE.isEnabled()) { diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 053ebed21b7..a6320bca493 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -1,24 +1,66 @@ package datadog.trace.api.rum; -import static java.util.concurrent.TimeUnit.SECONDS; - import datadog.trace.api.StatsDClient; -import datadog.trace.util.AgentTaskScheduler; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // This class implements the RumTelemetryCollector interface, which is used to collect telemetry -// from the RumInjector. Metrics are then reported via StatsDClient. +// from the RumInjector. Metrics are then reported via StatsDClient with tagging. See: +// https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/common_metrics.json +// for common metrics and tags. public class RumInjectorMetrics implements RumTelemetryCollector { private static final Logger log = LoggerFactory.getLogger(RumInjectorMetrics.class); private static final String[] NO_TAGS = new String[0]; - private final AtomicBoolean started = new AtomicBoolean(false); - private volatile AgentTaskScheduler.Scheduled cancellation; + // Use static tags for common combinations so that we don't have to build them for each metric + private static final String[] CSP_SERVLET3_TAGS = + new String[] { + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:3", + "kind:header", + "reason:csp_header_found", + "status:seen" + }; + + private static final String[] CSP_SERVLET5_TAGS = + new String[] { + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:5", + "kind:header", + "reason:csp_header_found", + "status:seen" + }; + + private static final String[] INIT_TAGS = + new String[] { + "injector_version:0.1.0", "integration_name:servlet", "integration_version:3,5" + }; + + private static final String[] TIME_SERVLET3_TAGS = + new String[] {"injector_version:0.1.0", "integration_name:servlet", "integration_version:3"}; + + private static final String[] TIME_SERVLET5_TAGS = + new String[] {"injector_version:0.1.0", "integration_name:servlet", "integration_version:5"}; + + private static final String[] RESPONSE_SERVLET3_TAGS = + new String[] { + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:3", + "response_kind:header" + }; + + private static final String[] RESPONSE_SERVLET5_TAGS = + new String[] { + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:5", + "response_kind:header" + }; private final AtomicLong injectionSucceed = new AtomicLong(); private final AtomicLong injectionFailed = new AtomicLong(); @@ -27,121 +69,128 @@ public class RumInjectorMetrics implements RumTelemetryCollector { private final AtomicLong initializationSucceed = new AtomicLong(); private final StatsDClient statsd; - private final long interval; - private final TimeUnit units; - - public void start() { - if (started.compareAndSet(false, true)) { - cancellation = - AgentTaskScheduler.INSTANCE.scheduleAtFixedRate( - new Flush(), this, interval, interval, units); - } - } - public RumInjectorMetrics(final StatsDClient statsd) { - this(statsd, 30, SECONDS); - } + private final String applicationId; + private final String remoteConfigUsed; - public RumInjectorMetrics(final StatsDClient statsd, long interval, TimeUnit units) { + public RumInjectorMetrics(final StatsDClient statsd) { this.statsd = statsd; - this.interval = interval; - this.units = units; + + // Get RUM config values (applicationId and remoteConfigUsed) for tagging + RumInjector rumInjector = RumInjector.get(); + if (rumInjector.isEnabled()) { + datadog.trace.api.Config config = datadog.trace.api.Config.get(); + RumInjectorConfig injectorConfig = config.getRumInjectorConfig(); + if (injectorConfig != null) { + this.applicationId = injectorConfig.applicationId; + this.remoteConfigUsed = injectorConfig.remoteConfigurationId != null ? "true" : "false"; + } else { + this.applicationId = "unknown"; + this.remoteConfigUsed = "false"; + } + } else { + this.applicationId = "unknown"; + this.remoteConfigUsed = "false"; + } } @Override - public void onInjectionSucceed() { + public void onInjectionSucceed(String integrationVersion) { injectionSucceed.incrementAndGet(); + + String[] tags = + new String[] { + "application_id:" + applicationId, + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:" + integrationVersion, + "remote_config_used:" + remoteConfigUsed + }; + + statsd.count("rum.injection.succeed", 1, tags); } @Override - public void onInjectionFailed() { + public void onInjectionFailed(String integrationVersion, String contentEncoding) { injectionFailed.incrementAndGet(); + + String[] tags = + new String[] { + "application_id:" + applicationId, + "content_encoding:" + contentEncoding, + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:" + integrationVersion, + "reason:failed_to_return_response_wrapper", + "remote_config_used:" + remoteConfigUsed + }; + + statsd.count("rum.injection.failed", 1, tags); } @Override - public void onInjectionSkipped() { + public void onInjectionSkipped(String integrationVersion) { injectionSkipped.incrementAndGet(); + + String[] tags = + new String[] { + "application_id:" + applicationId, + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:" + integrationVersion, + "reason:should_not_inject", + "remote_config_used:" + remoteConfigUsed + }; + + statsd.count("rum.injection.skipped", 1, tags); } @Override public void onInitializationSucceed() { initializationSucceed.incrementAndGet(); + statsd.count("rum.injection.initialization.succeed", 1, INIT_TAGS); } @Override - public void onContentSecurityPolicyDetected() { + public void onContentSecurityPolicyDetected(String integrationVersion) { contentSecurityPolicyDetected.incrementAndGet(); + + String[] tags = "5".equals(integrationVersion) ? CSP_SERVLET5_TAGS : CSP_SERVLET3_TAGS; + statsd.count("rum.injection.content_security_policy", 1, tags); } @Override - public void onInjectionResponseSize(long bytes) { - // report distribution metric immediately - statsd.distribution("rum.injection.response.bytes", bytes, NO_TAGS); + public void onInjectionResponseSize(String integrationVersion, long bytes) { + String[] tags = + "5".equals(integrationVersion) ? RESPONSE_SERVLET5_TAGS : RESPONSE_SERVLET3_TAGS; + statsd.distribution("rum.injection.response.bytes", bytes, tags); } @Override - public void onInjectionTime(long milliseconds) { - // report distribution metric immediately - statsd.distribution("rum.injection.ms", milliseconds, NO_TAGS); + public void onInjectionTime(String integrationVersion, long milliseconds) { + String[] tags = "5".equals(integrationVersion) ? TIME_SERVLET5_TAGS : TIME_SERVLET3_TAGS; + statsd.distribution("rum.injection.ms", milliseconds, tags); } + @Override public void close() { - if (null != cancellation) { - cancellation.cancel(); - } + injectionSucceed.set(0); + injectionFailed.set(0); + injectionSkipped.set(0); + contentSecurityPolicyDetected.set(0); + initializationSucceed.set(0); } public String summary() { - return "injectionSucceed=" + return "\ninitializationSucceed=" + + initializationSucceed.get() + + "\ninjectionSucceed=" + injectionSucceed.get() + "\ninjectionFailed=" + injectionFailed.get() + "\ninjectionSkipped=" + injectionSkipped.get() + "\ncontentSecurityPolicyDetected=" - + contentSecurityPolicyDetected.get() - + "\ninitializationSucceed=" - + initializationSucceed.get(); - } - - private static class Flush implements AgentTaskScheduler.Task { - - private final long[] previousCounts = new long[5]; // one per counter - private int countIndex; - - @Override - public void run(RumInjectorMetrics target) { - countIndex = -1; - try { - reportIfChanged(target.statsd, "rum.injection.succeed", target.injectionSucceed, NO_TAGS); - reportIfChanged(target.statsd, "rum.injection.failed", target.injectionFailed, NO_TAGS); - reportIfChanged(target.statsd, "rum.injection.skipped", target.injectionSkipped, NO_TAGS); - reportIfChanged( - target.statsd, - "rum.injection.content_security_policy", - target.contentSecurityPolicyDetected, - NO_TAGS); - reportIfChanged( - target.statsd, - "rum.injection.initialization.succeed", - target.initializationSucceed, - NO_TAGS); - } catch (ArrayIndexOutOfBoundsException e) { - log.warn( - "previousCounts array needs resizing to at least {}, was {}", - countIndex + 1, - previousCounts.length); - } - } - - private void reportIfChanged( - StatsDClient statsDClient, String aspect, AtomicLong counter, String[] tags) { - long count = counter.get(); - long delta = count - previousCounts[++countIndex]; - if (delta > 0) { - statsDClient.count(aspect, delta, tags); - previousCounts[countIndex] = count; - } - } + + contentSecurityPolicyDetected.get(); } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 970783796dd..c951184ca9b 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -7,28 +7,25 @@ public interface RumTelemetryCollector { RumTelemetryCollector NO_OP = new RumTelemetryCollector() { @Override - public void start() {} + public void onInjectionSucceed(String integrationVersion) {} @Override - public void onInjectionSucceed() {} + public void onInjectionFailed(String integrationVersion, String contentEncoding) {} @Override - public void onInjectionFailed() {} - - @Override - public void onInjectionSkipped() {} + public void onInjectionSkipped(String integrationVersion) {} @Override public void onInitializationSucceed() {} @Override - public void onContentSecurityPolicyDetected() {} + public void onContentSecurityPolicyDetected(String integrationVersion) {} @Override - public void onInjectionResponseSize(long bytes) {} + public void onInjectionResponseSize(String integrationVersion, long bytes) {} @Override - public void onInjectionTime(long milliseconds) {} + public void onInjectionTime(String integrationVersion, long milliseconds) {} @Override public void close() {} @@ -39,28 +36,26 @@ public String summary() { } }; - default void start() {} - // call when RUM injection succeeds - void onInjectionSucceed(); + void onInjectionSucceed(String integrationVersion); // call when RUM injection fails - void onInjectionFailed(); + void onInjectionFailed(String integrationVersion, String contentEncoding); // call when RUM injection is skipped - void onInjectionSkipped(); + void onInjectionSkipped(String integrationVersion); // call when RUM injector initialization succeeds void onInitializationSucceed(); // call when a Content Security Policy header is detected - void onContentSecurityPolicyDetected(); + void onContentSecurityPolicyDetected(String integrationVersion); // call to get the response size before RUM injection - void onInjectionResponseSize(long bytes); + void onInjectionResponseSize(String integrationVersion, long bytes); // call to report the time it takes to inject the RUM SDK - void onInjectionTime(long milliseconds); + void onInjectionTime(String integrationVersion, long milliseconds); default void close() {} diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index c319df14b84..a40015adb21 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -4,9 +4,6 @@ import datadog.trace.api.StatsDClient import spock.lang.Specification import spock.lang.Subject -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - class RumInjectorMetricsTest extends Specification { def statsD = Mock(StatsDClient) @@ -14,163 +11,186 @@ class RumInjectorMetricsTest extends Specification { def metrics = new RumInjectorMetrics(statsD) def "test onInjectionSucceed"() { - setup: - def latch = new CountDownLatch(1) - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - when: - metrics.onInjectionSucceed() - latch.await(5, TimeUnit.SECONDS) + metrics.onInjectionSucceed("3") + metrics.onInjectionSucceed("5") then: - 1 * statsD.count('rum.injection.succeed', 1, _) + 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + } + 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + } 0 * _ - - cleanup: - metrics.close() } def "test onInjectionFailed"() { - setup: - def latch = new CountDownLatch(1) - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - when: - metrics.onInjectionFailed() - latch.await(5, TimeUnit.SECONDS) + metrics.onInjectionFailed("3", "gzip") + metrics.onInjectionFailed("5", "none") then: - 1 * statsD.count('rum.injection.failed', 1, _) + 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("content_encoding:gzip") + assert tags.contains("reason:failed_to_return_response_wrapper") + } + 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("content_encoding:none") + assert tags.contains("reason:failed_to_return_response_wrapper") + } 0 * _ - - cleanup: - metrics.close() } def "test onInjectionSkipped"() { - setup: - def latch = new CountDownLatch(1) - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - when: - metrics.onInjectionSkipped() - latch.await(5, TimeUnit.SECONDS) + metrics.onInjectionSkipped("3") + metrics.onInjectionSkipped("5") then: - 1 * statsD.count('rum.injection.skipped', 1, _) + 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("reason:should_not_inject") + } + 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("reason:should_not_inject") + } 0 * _ - - cleanup: - metrics.close() } def "test onContentSecurityPolicyDetected"() { - setup: - def latch = new CountDownLatch(1) - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - when: - metrics.onContentSecurityPolicyDetected() - latch.await(5, TimeUnit.SECONDS) + metrics.onContentSecurityPolicyDetected("3") + metrics.onContentSecurityPolicyDetected("5") then: - 1 * statsD.count('rum.injection.content_security_policy', 1, _) + 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("kind:header") + assert tags.contains("reason:csp_header_found") + assert tags.contains("status:seen") + } + 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("kind:header") + assert tags.contains("reason:csp_header_found") + assert tags.contains("status:seen") + } 0 * _ - - cleanup: - metrics.close() } def "test onInitializationSucceed"() { - setup: - def latch = new CountDownLatch(1) - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - when: metrics.onInitializationSucceed() - latch.await(5, TimeUnit.SECONDS) then: - 1 * statsD.count('rum.injection.initialization.succeed', 1, _) + 1 * statsD.count('rum.injection.initialization.succeed', 1, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3,5") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + } 0 * _ - - cleanup: - metrics.close() } - def "test multiple events"() { - setup: - def latch = new CountDownLatch(5) // expecting 5 metric types - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - + def "test onInjectionResponseSize with multiple sizes"() { when: - metrics.onInjectionSucceed() - metrics.onInjectionFailed() - metrics.onInjectionSkipped() - metrics.onContentSecurityPolicyDetected() - metrics.onInitializationSucceed() - latch.await(5, TimeUnit.SECONDS) + metrics.onInjectionResponseSize("3", 256) + metrics.onInjectionResponseSize("3", 512) + metrics.onInjectionResponseSize("5", 2048) then: - 1 * statsD.count('rum.injection.succeed', 1, _) - 1 * statsD.count('rum.injection.failed', 1, _) - 1 * statsD.count('rum.injection.skipped', 1, _) - 1 * statsD.count('rum.injection.content_security_policy', 1, _) - 1 * statsD.count('rum.injection.initialization.succeed', 1, _) + 1 * statsD.distribution('rum.injection.response.bytes', 256, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + assert tags.contains("response_kind:header") + } + 1 * statsD.distribution('rum.injection.response.bytes', 512, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + assert tags.contains("response_kind:header") + } + 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + assert tags.contains("response_kind:header") + } 0 * _ - - cleanup: - metrics.close() } - def "test that flushing only reports non-zero deltas"() { - setup: - def latch = new CountDownLatch(1) // expecting only 1 metric call (non-zero delta) - def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS) - metrics.start() - + def "test onInjectionTime with multiple durations"() { when: - metrics.onInjectionSucceed() - metrics.onInjectionSucceed() - latch.await(5, TimeUnit.SECONDS) + metrics.onInjectionTime("5", 5L) + metrics.onInjectionTime("5", 10L) + metrics.onInjectionTime("3", 25L) then: - 1 * statsD.count('rum.injection.succeed', 2, _) - // should not be called since they have delta of 0 - 0 * statsD.count('rum.injection.failed', _, _) - 0 * statsD.count('rum.injection.skipped', _, _) - 0 * statsD.count('rum.injection.content_security_policy', _, _) + 1 * statsD.distribution('rum.injection.ms', 5L, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + } + 1 * statsD.distribution('rum.injection.ms', 10L, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:5") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + } + 1 * statsD.distribution('rum.injection.ms', 25L, _) >> { args -> + def tags = args[2] as String[] + assert tags.contains("integration_version:3") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + } 0 * _ - - cleanup: - metrics.close() } def "test summary with multiple events in different order"() { when: - metrics.onContentSecurityPolicyDetected() - metrics.onInjectionSucceed() - metrics.onInjectionFailed() - metrics.onInjectionSucceed() - metrics.onInjectionFailed() - metrics.onInjectionSucceed() - metrics.onInjectionSkipped() - metrics.onContentSecurityPolicyDetected() metrics.onInitializationSucceed() + metrics.onContentSecurityPolicyDetected("3") + metrics.onInjectionSkipped("5") + metrics.onInjectionFailed("3", "gzip") + metrics.onInjectionSucceed("3") + metrics.onInjectionFailed("5", "none") + metrics.onInjectionSucceed("3") + metrics.onInjectionSkipped("3") + metrics.onContentSecurityPolicyDetected("5") def summary = metrics.summary() then: - summary.contains("injectionSucceed=3") + summary.contains("initializationSucceed=1") + summary.contains("injectionSucceed=2") summary.contains("injectionFailed=2") - summary.contains("injectionSkipped=1") + summary.contains("injectionSkipped=2") summary.contains("contentSecurityPolicyDetected=2") - summary.contains("initializationSucceed=1") + 1 * statsD.count('rum.injection.initialization.succeed', 1, _) + 2 * statsD.count('rum.injection.succeed', 1, _) + 2 * statsD.count('rum.injection.failed', 1, _) + 2 * statsD.count('rum.injection.skipped', 1, _) + 2 * statsD.count('rum.injection.content_security_policy', 1, _) 0 * _ } @@ -179,157 +199,44 @@ class RumInjectorMetricsTest extends Specification { def summary = metrics.summary() then: + summary.contains("initializationSucceed=0") summary.contains("injectionSucceed=0") summary.contains("injectionFailed=0") summary.contains("injectionSkipped=0") summary.contains("contentSecurityPolicyDetected=0") - summary.contains("initializationSucceed=0") 0 * _ } - // events below are reported immediately as distribution metrics and do not get summarized - def "test onInjectionResponseSize with multiple sizes"() { + def "test close resets all counters"() { when: - metrics.onInjectionResponseSize(256) - metrics.onInjectionResponseSize(512) - metrics.onInjectionResponseSize(2048) - - then: - 1 * statsD.distribution('rum.injection.response.bytes', 256, _) - 1 * statsD.distribution('rum.injection.response.bytes', 512, _) - 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) - 0 * _ - } + metrics.onInitializationSucceed() + metrics.onInjectionSucceed("3") + metrics.onInjectionFailed("3", "gzip") + metrics.onInjectionSkipped("3") + metrics.onContentSecurityPolicyDetected("3") - def "test onInjectionTime with multiple durations"() { - when: - metrics.onInjectionTime(5L) - metrics.onInjectionTime(10L) - metrics.onInjectionTime(25L) + def summaryBeforeClose = metrics.summary() + metrics.close() + def summaryAfterClose = metrics.summary() then: - 1 * statsD.distribution('rum.injection.ms', 5L, _) - 1 * statsD.distribution('rum.injection.ms', 10L, _) - 1 * statsD.distribution('rum.injection.ms', 25L, _) - 0 * _ - } - - // taken from HealthMetricsTest - private static class Latched implements StatsDClient { - final StatsDClient delegate - final CountDownLatch latch - - Latched(StatsDClient delegate, CountDownLatch latch) { - this.delegate = delegate - this.latch = latch - } - - @Override - void incrementCounter(String metricName, String... tags) { - try { - delegate.incrementCounter(metricName, tags) - } finally { - latch.countDown() - } - } + summaryBeforeClose.contains("initializationSucceed=1") + summaryBeforeClose.contains("injectionSucceed=1") + summaryBeforeClose.contains("injectionFailed=1") + summaryBeforeClose.contains("injectionSkipped=1") + summaryBeforeClose.contains("contentSecurityPolicyDetected=1") + + summaryAfterClose.contains("initializationSucceed=0") + summaryAfterClose.contains("injectionSucceed=0") + summaryAfterClose.contains("injectionFailed=0") + summaryAfterClose.contains("injectionSkipped=0") + summaryAfterClose.contains("contentSecurityPolicyDetected=0") - @Override - void count(String metricName, long delta, String... tags) { - try { - delegate.count(metricName, delta, tags) - } finally { - latch.countDown() - } - } - - @Override - void gauge(String metricName, long value, String... tags) { - try { - delegate.gauge(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void gauge(String metricName, double value, String... tags) { - try { - delegate.gauge(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void histogram(String metricName, long value, String... tags) { - try { - delegate.histogram(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void histogram(String metricName, double value, String... tags) { - try { - delegate.histogram(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void distribution(String metricName, long value, String... tags) { - try { - delegate.distribution(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void distribution(String metricName, double value, String... tags) { - try { - delegate.distribution(metricName, value, tags) - } finally { - latch.countDown() - } - } - - @Override - void serviceCheck(String serviceCheckName, String status, String message, String... tags) { - try { - delegate.serviceCheck(serviceCheckName, status, message, tags) - } finally { - latch.countDown() - } - } - - @Override - void error(Exception error) { - try { - delegate.error(error) - } finally { - latch.countDown() - } - } - - @Override - int getErrorCount() { - try { - return delegate.getErrorCount() - } finally { - latch.countDown() - } - } - - @Override - void close() { - try { - delegate.close() - } finally { - latch.countDown() - } - } + 1 * statsD.count('rum.injection.initialization.succeed', 1, _) + 1 * statsD.count('rum.injection.succeed', 1, _) + 1 * statsD.count('rum.injection.failed', 1, _) + 1 * statsD.count('rum.injection.skipped', 1, _) + 1 * statsD.count('rum.injection.content_security_policy', 1, _) + 0 * _ } } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index 7a8f5dcc435..c7458b13e3b 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -122,7 +122,7 @@ class RumInjectorTest extends DDSpecification { when: RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) def telemetryCollector = RumInjector.getTelemetryCollector() - telemetryCollector.onContentSecurityPolicyDetected() + telemetryCollector.onContentSecurityPolicyDetected("3") def summary = telemetryCollector.summary() then: @@ -139,12 +139,12 @@ class RumInjectorTest extends DDSpecification { // simulate reporting successful injection def telemetryCollector = RumInjector.getTelemetryCollector() - telemetryCollector.onInjectionSucceed() - telemetryCollector.onInjectionFailed() - telemetryCollector.onInjectionSkipped() - telemetryCollector.onContentSecurityPolicyDetected() - telemetryCollector.onInjectionResponseSize(256) - telemetryCollector.onInjectionTime(5L) + telemetryCollector.onInjectionSucceed("3") + telemetryCollector.onInjectionFailed("3", "gzip") + telemetryCollector.onInjectionSkipped("3") + telemetryCollector.onContentSecurityPolicyDetected("3") + telemetryCollector.onInjectionResponseSize("3", 256) + telemetryCollector.onInjectionTime("3", 5L) // verify metrics are collected def summary = telemetryCollector.summary() @@ -154,7 +154,7 @@ class RumInjectorTest extends DDSpecification { summary.contains("injectionFailed=1") summary.contains("injectionSkipped=1") summary.contains("contentSecurityPolicyDetected=1") - summary.contains("initializationSucceed=0") // rum injector not initialized in test environment + summary.contains("initializationSucceed=0") // RUM injector not enabled in test environment cleanup: RumInjector.shutdownTelemetry() @@ -168,9 +168,9 @@ class RumInjectorTest extends DDSpecification { RumInjector.enableTelemetry(mockStatsDClient) def telemetryCollector = RumInjector.getTelemetryCollector() - telemetryCollector.onInjectionResponseSize(256) - telemetryCollector.onInjectionResponseSize(512) - telemetryCollector.onInjectionResponseSize(2048) + telemetryCollector.onInjectionResponseSize("3", 256) + telemetryCollector.onInjectionResponseSize("3", 512) + telemetryCollector.onInjectionResponseSize("5", 2048) then: // response sizes are reported immediately as distribution metrics @@ -188,9 +188,9 @@ class RumInjectorTest extends DDSpecification { RumInjector.enableTelemetry(mockStatsDClient) def telemetryCollector = RumInjector.getTelemetryCollector() - telemetryCollector.onInjectionTime(5L) - telemetryCollector.onInjectionTime(10L) - telemetryCollector.onInjectionTime(20L) + telemetryCollector.onInjectionTime("5", 5L) + telemetryCollector.onInjectionTime("5", 10L) + telemetryCollector.onInjectionTime("3", 20L) then: // injection times are reported immediately as distribution metrics @@ -210,12 +210,12 @@ class RumInjectorTest extends DDSpecification { // simulate multiple threads calling telemetry methods (1..50).each { i -> threads << Thread.start { - telemetryCollector.onInjectionSucceed() - telemetryCollector.onInjectionFailed() - telemetryCollector.onInjectionSkipped() - telemetryCollector.onContentSecurityPolicyDetected() - telemetryCollector.onInjectionResponseSize(256) - telemetryCollector.onInjectionTime(5L) + telemetryCollector.onInjectionSucceed("3") + telemetryCollector.onInjectionFailed("3", "gzip") + telemetryCollector.onInjectionSkipped("3") + telemetryCollector.onContentSecurityPolicyDetected("3") + telemetryCollector.onInjectionResponseSize("3", 256) + telemetryCollector.onInjectionTime("3", 5L) } } threads*.join() From b32874c5e68942ad87c9b124c6830704b12cc52b Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 8 Aug 2025 16:42:54 -0400 Subject: [PATCH 17/31] Update InjectingPipeOutputStreamTest --- .../buffer/InjectingPipeOutputStreamTest.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index 9b04234ad3d..1b6b5e11c2d 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -36,7 +36,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { def 'should filter a buffer and inject if found #found'() { setup: def downstream = new ByteArrayOutputStream() - def piped = new OutputStreamWriter(new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null), + def piped = new OutputStreamWriter(new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null, null), "UTF-8") when: try (def closeme = piped) { @@ -55,7 +55,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { setup: def baos = new ByteArrayOutputStream() def downstream = new GlitchedOutputStream(baos, glichesAt) - def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null) + def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null, null) when: try { for (String line : body) { From 7a5cd688ccfc116d1da96f3b8145d6baaefb7497 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 8 Aug 2025 17:30:40 -0400 Subject: [PATCH 18/31] Tweaks --- .../RumHttpServletResponseWrapper.java | 8 ++- .../RumHttpServletResponseWrapperTest.groovy | 16 +++--- .../RumHttpServletResponseWrapper.java | 10 ++-- .../RumHttpServletResponseWrapperTest.groovy | 16 +++--- .../datadog/trace/api/rum/RumInjector.java | 21 +++++-- .../trace/api/rum/RumInjectorMetrics.java | 9 +-- .../trace/api/rum/RumTelemetryCollector.java | 8 --- .../api/rum/RumInjectorMetricsTest.groovy | 55 ++++++++++--------- .../trace/api/rum/RumInjectorTest.groovy | 25 ++------- 9 files changed, 75 insertions(+), 93 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index 5877262c0c7..df2c788158a 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -67,6 +67,7 @@ public ServletOutputStream getOutputStream() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("3", bytes)); } catch (Exception e) { + injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed("3", contentEncoding); throw e; } @@ -96,6 +97,7 @@ public PrintWriter getWriter() throws IOException { this::onInjected); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { + injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed("3", contentEncoding); throw e; } @@ -109,7 +111,7 @@ public void setHeader(String name, String value) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("3"); - } else if (lowerName.equals("content-encoding")) { + } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } } @@ -122,7 +124,7 @@ public void addHeader(String name, String value) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("3"); - } else if (lowerName.equals("content-encoding")) { + } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } } @@ -169,7 +171,7 @@ public void resetBuffer() { public void onInjected() { RumInjector.getTelemetryCollector().onInjectionSucceed("3"); - // report injection time + // calculate total injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index db8ebb5462c..4b24e9b45ee 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -106,7 +106,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") 1 * mockResponse.setHeader("X-Content-Security-Policy", "test") } @@ -115,7 +115,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } @@ -161,16 +161,14 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { void 'injection timing is reported when injection is successful'() { setup: - wrapper.setContentType("text/html") + // set the injection start time to simulate timing + wrapper.@injectionStartTime = System.nanoTime() - 2_000_000L when: - try { - wrapper.getOutputStream() // set injectionStartTime - } catch (Exception ignored) {} // expect failure due to improper setup - Thread.sleep(1) // ensure some time passes - wrapper.onInjected() // report timing when injection "is successful" + wrapper.onInjected() // report timing when injection is successful then: - 1 * mockTelemetryCollector.onInjectionTime("3", { it >= 0 }) + 1 * mockTelemetryCollector.onInjectionSucceed("3") + 1 * mockTelemetryCollector.onInjectionTime("3", { it > 0 }) } } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 0f526d9068a..e5d0bf414f6 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -49,6 +49,7 @@ public ServletOutputStream getOutputStream() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("5", bytes)); } catch (Exception e) { + injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed("5", contentEncoding); throw e; } @@ -64,7 +65,7 @@ public PrintWriter getWriter() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped("5"); return super.getWriter(); } - // start timing installation + // start timing injection if (injectionStartTime == -1) { injectionStartTime = System.nanoTime(); } @@ -77,6 +78,7 @@ public PrintWriter getWriter() throws IOException { this::onInjected); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { + injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed("5", contentEncoding); throw e; } @@ -90,7 +92,7 @@ public void setHeader(String name, String value) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("5"); - } else if (lowerName.equals("content-encoding")) { + } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } } @@ -103,7 +105,7 @@ public void addHeader(String name, String value) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("5"); - } else if (lowerName.equals("content-encoding")) { + } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } } @@ -146,7 +148,7 @@ public void resetBuffer() { public void onInjected() { RumInjector.getTelemetryCollector().onInjectionSucceed("5"); - // report injection time + // calculate total injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 43d37e3c271..61248f41633 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -106,7 +106,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") 1 * mockResponse.setHeader("X-Content-Security-Policy", "test") } @@ -115,7 +115,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected() + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } @@ -161,16 +161,14 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { void 'injection timing is reported when injection is successful'() { setup: - wrapper.setContentType("text/html") + // set the injection start time to simulate timing + wrapper.@injectionStartTime = System.nanoTime() - 2_000_000L when: - try { - wrapper.getOutputStream() // set injectionStartTime - } catch (Exception ignored) {} // expect failure due to improper setup - Thread.sleep(1) // ensure some time passes - wrapper.onInjected() // report timing when injection "is successful" + wrapper.onInjected() // report timing when injection is successful then: - 1 * mockTelemetryCollector.onInjectionTime("5", { it >= 0 }) + 1 * mockTelemetryCollector.onInjectionSucceed("5") + 1 * mockTelemetryCollector.onInjectionTime("5", { it > 0 }) } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index 9b0b420508d..f813c019030 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -29,7 +29,6 @@ public final class RumInjector { private final DDCache markerCache; private final Function snippetBytes; - // telemetry collector defaults to NO_OP private static volatile RumTelemetryCollector telemetryCollector = RumTelemetryCollector.NO_OP; RumInjector(Config config, InstrumenterConfig instrumenterConfig) { @@ -126,7 +125,11 @@ public byte[] getMarkerBytes(String encoding) { return this.markerCache.computeIfAbsent(encoding, MARKER_BYTES); } - // start telemetry collection and report metrics via the given StatsDClient + /** + * Starts telemetry collection and reports metrics via StatsDClient. + * + * @param statsDClient The StatsDClient to report metrics to. + */ public static void enableTelemetry(datadog.trace.api.StatsDClient statsDClient) { if (statsDClient != null) { RumInjectorMetrics metrics = new RumInjectorMetrics(statsDClient); @@ -140,18 +143,26 @@ public static void enableTelemetry(datadog.trace.api.StatsDClient statsDClient) } } - // shutdown telemetry and reset to NO_OP + /** Shuts down telemetry collection and resets the telemetry collector to NO_OP. */ public static void shutdownTelemetry() { telemetryCollector.close(); telemetryCollector = RumTelemetryCollector.NO_OP; } - // set the telemetry collector + /** + * Sets the telemetry collector. + * + * @param collector The telemetry collector to set or {@code null} to reset to NO_OP. + */ public static void setTelemetryCollector(RumTelemetryCollector collector) { telemetryCollector = collector != null ? collector : RumTelemetryCollector.NO_OP; } - // get the telemetry collector. this is used to directly report telemetry + /** + * Gets the telemetry collector. + * + * @return The telemetry collector used to report telemetry. + */ public static RumTelemetryCollector getTelemetryCollector() { return telemetryCollector; } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index a6320bca493..17fcdb66929 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -2,18 +2,12 @@ import datadog.trace.api.StatsDClient; import java.util.concurrent.atomic.AtomicLong; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; // This class implements the RumTelemetryCollector interface, which is used to collect telemetry // from the RumInjector. Metrics are then reported via StatsDClient with tagging. See: // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/common_metrics.json // for common metrics and tags. public class RumInjectorMetrics implements RumTelemetryCollector { - private static final Logger log = LoggerFactory.getLogger(RumInjectorMetrics.class); - - private static final String[] NO_TAGS = new String[0]; - // Use static tags for common combinations so that we don't have to build them for each metric private static final String[] CSP_SERVLET3_TAGS = new String[] { @@ -79,8 +73,7 @@ public RumInjectorMetrics(final StatsDClient statsd) { // Get RUM config values (applicationId and remoteConfigUsed) for tagging RumInjector rumInjector = RumInjector.get(); if (rumInjector.isEnabled()) { - datadog.trace.api.Config config = datadog.trace.api.Config.get(); - RumInjectorConfig injectorConfig = config.getRumInjectorConfig(); + RumInjectorConfig injectorConfig = datadog.trace.api.Config.get().getRumInjectorConfig(); if (injectorConfig != null) { this.applicationId = injectorConfig.applicationId; this.remoteConfigUsed = injectorConfig.remoteConfigurationId != null ? "true" : "false"; diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index c951184ca9b..c6c6034e8c3 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -36,30 +36,22 @@ public String summary() { } }; - // call when RUM injection succeeds void onInjectionSucceed(String integrationVersion); - // call when RUM injection fails void onInjectionFailed(String integrationVersion, String contentEncoding); - // call when RUM injection is skipped void onInjectionSkipped(String integrationVersion); - // call when RUM injector initialization succeeds void onInitializationSucceed(); - // call when a Content Security Policy header is detected void onContentSecurityPolicyDetected(String integrationVersion); - // call to get the response size before RUM injection void onInjectionResponseSize(String integrationVersion, long bytes); - // call to report the time it takes to inject the RUM SDK void onInjectionTime(String integrationVersion, long milliseconds); default void close() {} - // human-readable summary of the current health metrics default String summary() { return ""; } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index a40015adb21..cdf7c38727f 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -10,6 +10,8 @@ class RumInjectorMetricsTest extends Specification { @Subject def metrics = new RumInjectorMetrics(statsD) + // Note: application_id and remote_config_used are dynamic runtime values that depend on + // the RUM configuration state, so we do not test them here. def "test onInjectionSucceed"() { when: metrics.onInjectionSucceed("3") @@ -18,15 +20,15 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:3") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:3") } 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:5") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:5") } 0 * _ } @@ -39,14 +41,18 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:3") assert tags.contains("content_encoding:gzip") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:3") assert tags.contains("reason:failed_to_return_response_wrapper") } 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:5") assert tags.contains("content_encoding:none") + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:5") assert tags.contains("reason:failed_to_return_response_wrapper") } 0 * _ @@ -60,11 +66,15 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> def tags = args[2] as String[] + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") assert tags.contains("integration_version:3") assert tags.contains("reason:should_not_inject") } 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> def tags = args[2] as String[] + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") assert tags.contains("integration_version:5") assert tags.contains("reason:should_not_inject") } @@ -79,6 +89,8 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> def tags = args[2] as String[] + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") assert tags.contains("integration_version:3") assert tags.contains("kind:header") assert tags.contains("reason:csp_header_found") @@ -86,6 +98,8 @@ class RumInjectorMetricsTest extends Specification { } 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> def tags = args[2] as String[] + assert tags.contains("injector_version:0.1.0") + assert tags.contains("integration_name:servlet") assert tags.contains("integration_version:5") assert tags.contains("kind:header") assert tags.contains("reason:csp_header_found") @@ -101,9 +115,9 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.initialization.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:3,5") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:3,5") } 0 * _ } @@ -111,29 +125,21 @@ class RumInjectorMetricsTest extends Specification { def "test onInjectionResponseSize with multiple sizes"() { when: metrics.onInjectionResponseSize("3", 256) - metrics.onInjectionResponseSize("3", 512) - metrics.onInjectionResponseSize("5", 2048) + metrics.onInjectionResponseSize("5", 512) then: 1 * statsD.distribution('rum.injection.response.bytes', 256, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:3") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:3") assert tags.contains("response_kind:header") } 1 * statsD.distribution('rum.injection.response.bytes', 512, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:3") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") - assert tags.contains("response_kind:header") - } - 1 * statsD.distribution('rum.injection.response.bytes', 2048, _) >> { args -> - def tags = args[2] as String[] assert tags.contains("integration_version:5") - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") assert tags.contains("response_kind:header") } 0 * _ @@ -142,27 +148,20 @@ class RumInjectorMetricsTest extends Specification { def "test onInjectionTime with multiple durations"() { when: metrics.onInjectionTime("5", 5L) - metrics.onInjectionTime("5", 10L) - metrics.onInjectionTime("3", 25L) + metrics.onInjectionTime("3", 10L) then: 1 * statsD.distribution('rum.injection.ms', 5L, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:5") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") + assert tags.contains("integration_version:5") } 1 * statsD.distribution('rum.injection.ms', 10L, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("integration_version:5") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") - } - 1 * statsD.distribution('rum.injection.ms', 25L, _) >> { args -> - def tags = args[2] as String[] assert tags.contains("integration_version:3") - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") } 0 * _ } @@ -178,6 +177,8 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionSucceed("3") metrics.onInjectionSkipped("3") metrics.onContentSecurityPolicyDetected("5") + metrics.onInjectionResponseSize("3", 256) + metrics.onInjectionTime("5", 5L) def summary = metrics.summary() then: @@ -191,10 +192,12 @@ class RumInjectorMetricsTest extends Specification { 2 * statsD.count('rum.injection.failed', 1, _) 2 * statsD.count('rum.injection.skipped', 1, _) 2 * statsD.count('rum.injection.content_security_policy', 1, _) + 1 * statsD.distribution('rum.injection.response.bytes', 256, _) + 1 * statsD.distribution('rum.injection.ms', 5L, _) 0 * _ } - def "test metrics start at zero"() { + def "test metrics start at zero in summary"() { when: def summary = metrics.summary() @@ -207,7 +210,7 @@ class RumInjectorMetricsTest extends Specification { 0 * _ } - def "test close resets all counters"() { + def "test close resets counters in summary"() { when: metrics.onInitializationSucceed() metrics.onInjectionSucceed("3") diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index c7458b13e3b..e16939adf8b 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -118,15 +118,15 @@ class RumInjectorTest extends DDSpecification { RumInjector.getTelemetryCollector() == RumTelemetryCollector.NO_OP } - void 'content security policy HTTP response detected'() { + void 'initialize rum injector'() { when: RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) def telemetryCollector = RumInjector.getTelemetryCollector() - telemetryCollector.onContentSecurityPolicyDetected("3") + telemetryCollector.onInitializationSucceed() def summary = telemetryCollector.summary() then: - summary.contains("contentSecurityPolicyDetected=1") + summary.contains("initializationSucceed=1") cleanup: RumInjector.shutdownTelemetry() @@ -137,7 +137,7 @@ class RumInjectorTest extends DDSpecification { // simulate CoreTracer enabling telemetry RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) - // simulate reporting successful injection + // simulate reporting injection telemetry def telemetryCollector = RumInjector.getTelemetryCollector() telemetryCollector.onInjectionSucceed("3") telemetryCollector.onInjectionFailed("3", "gzip") @@ -154,7 +154,6 @@ class RumInjectorTest extends DDSpecification { summary.contains("injectionFailed=1") summary.contains("injectionSkipped=1") summary.contains("contentSecurityPolicyDetected=1") - summary.contains("initializationSucceed=0") // RUM injector not enabled in test environment cleanup: RumInjector.shutdownTelemetry() @@ -173,7 +172,6 @@ class RumInjectorTest extends DDSpecification { telemetryCollector.onInjectionResponseSize("5", 2048) then: - // response sizes are reported immediately as distribution metrics noExceptionThrown() cleanup: @@ -193,7 +191,6 @@ class RumInjectorTest extends DDSpecification { telemetryCollector.onInjectionTime("3", 20L) then: - // injection times are reported immediately as distribution metrics noExceptionThrown() cleanup: @@ -231,18 +228,4 @@ class RumInjectorTest extends DDSpecification { cleanup: RumInjector.shutdownTelemetry() } - - void 'initialize rum injector successfully'() { - when: - RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) - def telemetryCollector = RumInjector.getTelemetryCollector() - telemetryCollector.onInitializationSucceed() - def summary = telemetryCollector.summary() - - then: - summary.contains("initializationSucceed=1") - - cleanup: - RumInjector.shutdownTelemetry() - } } From 412a3f82434a600c263cd4ecbae1c5403870124d Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Sat, 9 Aug 2025 15:45:50 -0400 Subject: [PATCH 19/31] Address jacoco coverage and injectingpipeoutstream interface updates --- .../InjectingPipeOutputStreamBenchmark.java | 3 +- internal-api/build.gradle.kts | 2 + .../trace/api/rum/RumTelemetryCollector.java | 36 ++++++ .../api/rum/RumTelemetryCollectorTest.groovy | 106 ++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy diff --git a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java index 8b90b4678a7..bec95c8a938 100644 --- a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java +++ b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java @@ -51,7 +51,8 @@ public class InjectingPipeOutputStreamBenchmark { public void withPipe() throws Exception { try (final PrintWriter out = new PrintWriter( - new InjectingPipeOutputStream(new ByteArrayOutputStream(), marker, content, null))) { + new InjectingPipeOutputStream( + new ByteArrayOutputStream(), marker, content, null, null))) { htmlContent.forEach(out::println); } } diff --git a/internal-api/build.gradle.kts b/internal-api/build.gradle.kts index eb25ebd9ac8..6b29c32d3a7 100644 --- a/internal-api/build.gradle.kts +++ b/internal-api/build.gradle.kts @@ -250,6 +250,8 @@ val excludedClassesBranchCoverage by extra( "datadog.trace.api.env.CapturedEnvironment.ProcessInfo", "datadog.trace.util.TempLocationManager", "datadog.trace.util.TempLocationManager.*", + // Branches depend on RUM injector state that cannot be reliably controlled in unit tests + "datadog.trace.api.rum.RumInjectorMetrics", ) ) diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index c6c6034e8c3..94ced1afa57 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -36,22 +36,58 @@ public String summary() { } }; + /** + * Reports successful RUM injection. + * + * @param integrationVersion The version of the integration that was injected. + */ void onInjectionSucceed(String integrationVersion); + /** + * Reports failed RUM injection. + * + * @param integrationVersion The version of the integration that was injected. + * @param contentEncoding The content encoding of the response that was injected. + */ void onInjectionFailed(String integrationVersion, String contentEncoding); + /** + * Reports skipped RUM injection. + * + * @param integrationVersion The version of the integration that was injected. + */ void onInjectionSkipped(String integrationVersion); + /** Reports successful RUM injector initialization. */ void onInitializationSucceed(); + /** + * Reports content security policy detected in the response header to be injected. + * + * @param integrationVersion The version of the integration that was injected. + */ void onContentSecurityPolicyDetected(String integrationVersion); + /** + * Reports the size of the response before injection. + * + * @param integrationVersion The version of the integration that was injected. + * @param bytes The size of the response before injection. + */ void onInjectionResponseSize(String integrationVersion, long bytes); + /** + * Reports the time taken to inject the RUM SDK. + * + * @param integrationVersion The version of the integration that was injected. + * @param milliseconds The time taken to inject the RUM SDK. + */ void onInjectionTime(String integrationVersion, long milliseconds); + /** Closes the telemetry collector. */ default void close() {} + /** Returns a human-readable summary of the telemetry collected. */ default String summary() { return ""; } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy new file mode 100644 index 00000000000..3f6215402fa --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy @@ -0,0 +1,106 @@ +package datadog.trace.api.rum + +import spock.lang.Specification + +class RumTelemetryCollectorTest extends Specification { + + def "test default NO_OP does not throw exception"() { + when: + RumTelemetryCollector.NO_OP.onInjectionSucceed("3") + RumTelemetryCollector.NO_OP.onInjectionSucceed("5") + RumTelemetryCollector.NO_OP.onInjectionFailed("3", "gzip") + RumTelemetryCollector.NO_OP.onInjectionFailed("5", "none") + RumTelemetryCollector.NO_OP.onInjectionSkipped("3") + RumTelemetryCollector.NO_OP.onInjectionSkipped("5") + RumTelemetryCollector.NO_OP.onInitializationSucceed() + RumTelemetryCollector.NO_OP.onContentSecurityPolicyDetected("3") + RumTelemetryCollector.NO_OP.onContentSecurityPolicyDetected("5") + RumTelemetryCollector.NO_OP.onInjectionResponseSize("3", 256L) + RumTelemetryCollector.NO_OP.onInjectionResponseSize("5", 512L) + RumTelemetryCollector.NO_OP.onInjectionTime("3", 10L) + RumTelemetryCollector.NO_OP.onInjectionTime("5", 20L) + RumTelemetryCollector.NO_OP.close() + + then: + noExceptionThrown() + } + + def "test default NO_OP summary returns an empty string"() { + when: + def summary = RumTelemetryCollector.NO_OP.summary() + + then: + summary == "" + } + + def "test default NO_OP close method does not throw exception"() { + when: + RumTelemetryCollector.NO_OP.close() + + then: + noExceptionThrown() + } + + def "test defining a custom implementation does not throw exception"() { + setup: + def customCollector = new RumTelemetryCollector() { + @Override + void onInjectionSucceed(String integrationVersion) { + } + + @Override + void onInjectionFailed(String integrationVersion, String contentEncoding) { + } + + @Override + void onInjectionSkipped(String integrationVersion) { + } + + @Override + void onInitializationSucceed() { + } + + @Override + void onContentSecurityPolicyDetected(String integrationVersion) { + } + + @Override + void onInjectionResponseSize(String integrationVersion, long bytes) { + } + + @Override + void onInjectionTime(String integrationVersion, long milliseconds) { + } + } + + when: + customCollector.close() + def summary = customCollector.summary() + + then: + noExceptionThrown() + summary == "" + } + + def "test multiple close calls do not throw exception"() { + when: + RumTelemetryCollector.NO_OP.close() + RumTelemetryCollector.NO_OP.close() + RumTelemetryCollector.NO_OP.close() + + then: + noExceptionThrown() + } + + def "test multiple summary calls return the same empty string"() { + when: + def summary1 = RumTelemetryCollector.NO_OP.summary() + def summary2 = RumTelemetryCollector.NO_OP.summary() + def summary3 = RumTelemetryCollector.NO_OP.summary() + + then: + summary1 == "" + summary1 == summary2 + summary2 == summary3 + } +} From b3fcde4cee7e9c022fdf4f623cc2abff8bb0395b Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 11 Aug 2025 10:28:51 -0400 Subject: [PATCH 20/31] Add content-length detection for InjectingPipeWriter and improve tests --- .../buffer/InjectingPipeWriter.java | 18 +++- .../InjectingPipeOutputStreamTest.groovy | 90 +++++++++++++++++ .../buffer/InjectingPipeWriterTest.groovy | 96 ++++++++++++++++++- .../RumHttpServletResponseWrapper.java | 3 +- .../RumHttpServletResponseWrapperTest.groovy | 18 ++++ .../RumHttpServletResponseWrapper.java | 3 +- .../RumHttpServletResponseWrapperTest.groovy | 18 ++++ 7 files changed, 240 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index 7a3d4a75f19..d39b81808a1 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.Writer; +import java.util.function.LongConsumer; import javax.annotation.concurrent.NotThreadSafe; /** @@ -23,18 +24,22 @@ public class InjectingPipeWriter extends Writer { private final Runnable onContentInjected; private final int bulkWriteThreshold; private final Writer downstream; + private final LongConsumer onBytesWritten; + private long bytesWritten = 0; /** * @param downstream the delegate writer * @param marker the marker to find in the stream. Must at least be one char. * @param contentToInject the content to inject once before the marker if found. * @param onContentInjected callback called when and if the content is injected. + * @param onBytesWritten callback called when writer is closed to report total bytes written. */ public InjectingPipeWriter( final Writer downstream, final char[] marker, final char[] contentToInject, - final Runnable onContentInjected) { + final Runnable onContentInjected, + final LongConsumer onBytesWritten) { this.downstream = downstream; this.marker = marker; this.lookbehind = new char[marker.length]; @@ -46,11 +51,13 @@ public InjectingPipeWriter( this.filter = true; this.contentToInject = contentToInject; this.onContentInjected = onContentInjected; + this.onBytesWritten = onBytesWritten; this.bulkWriteThreshold = marker.length * 2 - 2; } @Override public void write(int c) throws IOException { + bytesWritten++; if (!filter) { if (wasDraining) { // continue draining @@ -85,6 +92,7 @@ public void write(int c) throws IOException { @Override public void write(char[] array, int off, int len) throws IOException { + bytesWritten += len; if (!filter) { if (wasDraining) { // needs drain @@ -113,6 +121,7 @@ public void write(char[] array, int off, int len) throws IOException { // we don't have a full match. write everything in a bulk except the lookbehind buffer // sequentially for (int i = off; i < off + marker.length - 1; i++) { + bytesWritten--; // avoid double counting write(array[i]); } drain(); @@ -124,12 +133,14 @@ public void write(char[] array, int off, int len) throws IOException { filter = wasFiltering; for (int i = len - marker.length + 1; i < len; i++) { + bytesWritten--; // avoid double counting write(array[i]); } } } else { // use slow path because the length to write is small and within the lookbehind buffer size for (int i = off; i < off + len; i++) { + bytesWritten--; // avoid double counting write(array[i]); } } @@ -188,6 +199,11 @@ public void close() throws IOException { commit(); } finally { downstream.close(); + // report the size of the original HTTP response before injecting via callback + if (onBytesWritten != null) { + onBytesWritten.accept(bytesWritten); + } + bytesWritten = 0; } } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index 1b6b5e11c2d..451727b0159 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -87,4 +87,94 @@ class InjectingPipeOutputStreamTest extends DDSpecification { // expected broken since the real write happens at close (drain) being the content smaller than the buffer. And retry on close is not a common practice. Hence, we suppose loosing content [""] | "" | "" | 3 | " bytesWritten.add(bytes) } + def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + + when: + piped.write("test content".getBytes("UTF-8")) + piped.close() + + then: + bytesWritten.size() == 1 + bytesWritten[0] == 12 + downstream.toByteArray() == "test content".getBytes("UTF-8") + } + + def 'should count bytes correctly when writing bytes individually'() { + setup: + def downstream = new ByteArrayOutputStream() + def bytesWritten = [] + def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } + def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + + when: + def bytes = "test".getBytes("UTF-8") + for (int i = 0; i < bytes.length; i++) { + piped.write((int) bytes[i]) + } + piped.close() + + then: + bytesWritten.size() == 1 + bytesWritten[0] == 4 + downstream.toByteArray() == "test".getBytes("UTF-8") + } + + def 'should count bytes correctly with multiple writes'() { + setup: + def downstream = new ByteArrayOutputStream() + def bytesWritten = [] + def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } + def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + + when: + piped.write("test".getBytes("UTF-8")) + piped.write(" ".getBytes("UTF-8")) + piped.write("content".getBytes("UTF-8")) + piped.close() + + then: + bytesWritten.size() == 1 + bytesWritten[0] == 12 + downstream.toByteArray() == "test content".getBytes("UTF-8") + } + + def 'should be resilient to exceptions when onBytesWritten callback is null'() { + setup: + def downstream = new ByteArrayOutputStream() + def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, null) + + when: + piped.write("test content".getBytes("UTF-8")) + piped.close() + + then: + noExceptionThrown() + downstream.toByteArray() == "test content".getBytes("UTF-8") + } + + def 'should reset byte count after close'() { + setup: + def downstream = new ByteArrayOutputStream() + def bytesWritten = [] + def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } + def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + + when: + piped.write("test".getBytes("UTF-8")) + piped.close() + + piped.write("content".getBytes("UTF-8")) + piped.close() + + then: + bytesWritten.size() == 2 + bytesWritten[0] == 4 + bytesWritten[1] == 7 + } } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index d115f81a403..94909cac47d 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -36,7 +36,7 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should filter a buffer and inject if found #found using write'() { setup: def downstream = new StringWriter() - def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null)) + def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null, null)) when: try (def closeme = piped) { piped.write(body) @@ -53,7 +53,7 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should filter a buffer and inject if found #found using append'() { setup: def downstream = new StringWriter() - def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null)) + def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null, null)) when: try (def closeme = piped) { piped.append(body) @@ -71,7 +71,7 @@ class InjectingPipeWriterTest extends DDSpecification { setup: def writer = new StringWriter() def downstream = new GlitchedWriter(writer, glichesAt) - def piped = new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null) + def piped = new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null, null) when: try { for (String line : body) { @@ -103,4 +103,94 @@ class InjectingPipeWriterTest extends DDSpecification { // expected broken since the real write happens at close (drain) being the content smaller than the buffer. And retry on close is not a common practice. Hence, we suppose loosing content [""] | "" | "" | 3 | " bytesWritten.add(bytes) } + def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + + when: + piped.write("test content".toCharArray()) + piped.close() + + then: + bytesWritten.size() == 1 + bytesWritten[0] == 12 + downstream.toString() == "test content" + } + + def 'should count bytes correctly when writing characters individually'() { + setup: + def downstream = new StringWriter() + def bytesWritten = [] + def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } + def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + + when: + def content = "test" + for (int i = 0; i < content.length(); i++) { + piped.write((int) content.charAt(i)) + } + piped.close() + + then: + bytesWritten.size() == 1 + bytesWritten[0] == 4 + downstream.toString() == "test" + } + + def 'should count bytes correctly with multiple writes'() { + setup: + def downstream = new StringWriter() + def bytesWritten = [] + def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } + def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + + when: + piped.write("test".toCharArray()) + piped.write(" ".toCharArray()) + piped.write("content".toCharArray()) + piped.close() + + then: + bytesWritten.size() == 1 + bytesWritten[0] == 12 + downstream.toString() == "test content" + } + + def 'should be resilient to exceptions when onBytesWritten callback is null'() { + setup: + def downstream = new StringWriter() + def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, null) + + when: + piped.write("test content".toCharArray()) + piped.close() + + then: + noExceptionThrown() + downstream.toString() == "test content" + } + + def 'should reset byte count after close'() { + setup: + def downstream = new StringWriter() + def bytesWritten = [] + def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } + def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + + when: + piped.write("test".toCharArray()) + piped.close() + + piped.write("content".toCharArray()) + piped.close() + + then: + bytesWritten.size() == 2 + bytesWritten[0] == 4 + bytesWritten[1] == 7 + } } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index df2c788158a..dd79548fd82 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -94,7 +94,8 @@ public PrintWriter getWriter() throws IOException { super.getWriter(), rumInjector.getMarkerChars(), rumInjector.getSnippetChars(), - this::onInjected); + this::onInjected, + bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("3", bytes)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { injectionStartTime = -1; diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 4b24e9b45ee..07af7fcc876 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -159,6 +159,24 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * onBytesWritten.accept(11) } + void 'response sizes are reported by the InjectingPipeWriter callback'() { + setup: + def downstream = Mock(java.io.Writer) + def marker = "".toCharArray() + def contentToInject = "".toCharArray() + def onBytesWritten = Mock(java.util.function.LongConsumer) + def writer = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter( + downstream, marker, contentToInject, null, onBytesWritten) + + when: + writer.write("test".toCharArray()) + writer.write("content".toCharArray()) + writer.close() + + then: + 1 * onBytesWritten.accept(11) + } + void 'injection timing is reported when injection is successful'() { setup: // set the injection start time to simulate timing diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index e5d0bf414f6..2d8013676e7 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -75,7 +75,8 @@ public PrintWriter getWriter() throws IOException { super.getWriter(), rumInjector.getMarkerChars(), rumInjector.getSnippetChars(), - this::onInjected); + this::onInjected, + bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("5", bytes)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { injectionStartTime = -1; diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 61248f41633..d878df85f0d 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -159,6 +159,24 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * onBytesWritten.accept(11) } + void 'response sizes are reported by the InjectingPipeWriter callback'() { + setup: + def downstream = Mock(java.io.Writer) + def marker = "".toCharArray() + def contentToInject = "".toCharArray() + def onBytesWritten = Mock(java.util.function.LongConsumer) + def writer = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter( + downstream, marker, contentToInject, null, onBytesWritten) + + when: + writer.write("test".toCharArray()) + writer.write("content".toCharArray()) + writer.close() + + then: + 1 * onBytesWritten.accept(11) + } + void 'injection timing is reported when injection is successful'() { setup: // set the injection start time to simulate timing From 4b2f6b3515f5f0180bdce1dec65b7aa6aafd95dd Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 11 Aug 2025 17:45:57 -0400 Subject: [PATCH 21/31] Address review comments --- .../buffer/InjectingPipeOutputStream.java | 12 ++++---- .../buffer/InjectingPipeWriter.java | 12 ++++---- .../RumHttpServletResponseWrapper.java | 27 ++++++++++-------- .../RumHttpServletResponseWrapperTest.groovy | 27 +++++++++--------- .../RumHttpServletResponseWrapper.java | 28 +++++++++++-------- .../RumHttpServletResponseWrapperTest.groovy | 27 +++++++++--------- .../datadog/trace/api/rum/RumInjector.java | 2 +- .../trace/api/rum/RumInjectorMetrics.java | 26 ++++++++--------- .../trace/api/rum/RumTelemetryCollector.java | 6 ++-- .../api/rum/RumInjectorMetricsTest.groovy | 6 ++-- .../api/rum/RumTelemetryCollectorTest.groovy | 2 +- 11 files changed, 97 insertions(+), 78 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index 72a7c527847..b1f06d3c1f9 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -57,18 +57,19 @@ public InjectingPipeOutputStream( @Override public void write(int b) throws IOException { - bytesWritten++; if (!filter) { if (wasDraining) { // continue draining drain(); } downstream.write(b); + bytesWritten++; return; } if (count == lookbehind.length) { downstream.write(lookbehind[pos]); + bytesWritten++; } else { count++; } @@ -92,13 +93,13 @@ public void write(int b) throws IOException { @Override public void write(byte[] array, int off, int len) throws IOException { - bytesWritten += len; if (!filter) { if (wasDraining) { // needs drain drain(); } downstream.write(array, off, len); + bytesWritten += len; return; } @@ -112,16 +113,17 @@ public void write(byte[] array, int off, int len) throws IOException { filter = false; drain(); downstream.write(array, off, idx); + bytesWritten += idx; downstream.write(contentToInject); if (onContentInjected != null) { onContentInjected.run(); } downstream.write(array, off + idx, len - idx); + bytesWritten += (len - idx); } else { // we don't have a full match. write everything in a bulk except the lookbehind buffer // sequentially for (int i = off; i < off + marker.length - 1; i++) { - bytesWritten--; // avoid double counting write(array[i]); } drain(); @@ -130,16 +132,15 @@ public void write(byte[] array, int off, int len) throws IOException { // will be reset if no errors after the following write filter = false; downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold); + bytesWritten += (len - bulkWriteThreshold); filter = wasFiltering; for (int i = len - marker.length + 1; i < len; i++) { - bytesWritten--; // avoid double counting write(array[i]); } } } else { // use slow path because the length to write is small and within the lookbehind buffer size for (int i = off; i < off + len; i++) { - bytesWritten--; // avoid double counting write(array[i]); } } @@ -174,6 +175,7 @@ private void drain() throws IOException { int cnt = count; for (int i = 0; i < cnt; i++) { downstream.write(lookbehind[(start + i) % lookbehind.length]); + bytesWritten++; count--; } filter = wasFiltering; diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index d39b81808a1..adea204e054 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -57,18 +57,19 @@ public InjectingPipeWriter( @Override public void write(int c) throws IOException { - bytesWritten++; if (!filter) { if (wasDraining) { // continue draining drain(); } downstream.write(c); + bytesWritten++; return; } if (count == lookbehind.length) { downstream.write(lookbehind[pos]); + bytesWritten++; } else { count++; } @@ -92,13 +93,13 @@ public void write(int c) throws IOException { @Override public void write(char[] array, int off, int len) throws IOException { - bytesWritten += len; if (!filter) { if (wasDraining) { // needs drain drain(); } downstream.write(array, off, len); + bytesWritten += len; return; } @@ -112,16 +113,17 @@ public void write(char[] array, int off, int len) throws IOException { filter = false; drain(); downstream.write(array, off, idx); + bytesWritten += idx; downstream.write(contentToInject); if (onContentInjected != null) { onContentInjected.run(); } downstream.write(array, off + idx, len - idx); + bytesWritten += (len - idx); } else { // we don't have a full match. write everything in a bulk except the lookbehind buffer // sequentially for (int i = off; i < off + marker.length - 1; i++) { - bytesWritten--; // avoid double counting write(array[i]); } drain(); @@ -130,17 +132,16 @@ public void write(char[] array, int off, int len) throws IOException { // will be reset if no errors after the following write filter = false; downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold); + bytesWritten += (len - bulkWriteThreshold); filter = wasFiltering; for (int i = len - marker.length + 1; i < len; i++) { - bytesWritten--; // avoid double counting write(array[i]); } } } else { // use slow path because the length to write is small and within the lookbehind buffer size for (int i = off; i < off + len; i++) { - bytesWritten--; // avoid double counting write(array[i]); } } @@ -175,6 +176,7 @@ private void drain() throws IOException { int cnt = count; for (int i = 0; i < cnt; i++) { downstream.write(lookbehind[(start + i) % lookbehind.length]); + bytesWritten++; count--; } filter = wasFiltering; diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index dd79548fd82..af4ce3234ab 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -19,9 +19,10 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private InjectingPipeWriter wrappedPipeWriter; private boolean shouldInject = true; private long injectionStartTime = -1; - private String contentEncoding = "none"; + private String contentEncoding = null; private static final MethodHandle SET_CONTENT_LENGTH_LONG = getMh("setContentLengthLong"); + private static final String SERVLET_VERSION = "3"; private static MethodHandle getMh(final String name) { try { @@ -47,7 +48,7 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped("3"); + RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); return super.getOutputStream(); } // start timing injection @@ -65,10 +66,12 @@ public ServletOutputStream getOutputStream() throws IOException { rumInjector.getMarkerBytes(encoding), rumInjector.getSnippetBytes(encoding), this::onInjected, - bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("3", bytes)); + bytes -> + RumInjector.getTelemetryCollector() + .onInjectionResponseSize(SERVLET_VERSION, bytes)); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed("3", contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); throw e; } @@ -81,7 +84,7 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped("3"); + RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); return super.getWriter(); } // start timing injection @@ -95,11 +98,13 @@ public PrintWriter getWriter() throws IOException { rumInjector.getMarkerChars(), rumInjector.getSnippetChars(), this::onInjected, - bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("3", bytes)); + bytes -> + RumInjector.getTelemetryCollector() + .onInjectionResponseSize(SERVLET_VERSION, bytes)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed("3", contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); throw e; } @@ -111,7 +116,7 @@ public void setHeader(String name, String value) { if (name != null) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("3"); + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } @@ -124,7 +129,7 @@ public void addHeader(String name, String value) { if (name != null) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("3"); + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } @@ -170,13 +175,13 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.getTelemetryCollector().onInjectionSucceed("3"); + RumInjector.getTelemetryCollector().onInjectionSucceed(SERVLET_VERSION); // calculate total injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime("3", milliseconds); + RumInjector.getTelemetryCollector().onInjectionTime(SERVLET_VERSION, milliseconds); injectionStartTime = -1; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 07af7fcc876..50135d0ecce 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -7,6 +7,7 @@ import spock.lang.Subject import javax.servlet.http.HttpServletResponse class RumHttpServletResponseWrapperTest extends AgentTestRunner { + private static final String SERVLET_VERSION = "3" def mockResponse = Mock(HttpServletResponse) def mockTelemetryCollector = Mock(RumTelemetryCollector) @@ -28,7 +29,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() then: - 1 * mockTelemetryCollector.onInjectionSucceed("3") + 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) } void 'getOutputStream with non-HTML content reports skipped'() { @@ -39,7 +40,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getOutputStream() then: - 1 * mockTelemetryCollector.onInjectionSkipped("3") + 1 * mockTelemetryCollector.onInjectionSkipped(SERVLET_VERSION) 1 * mockResponse.getOutputStream() } @@ -51,7 +52,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getWriter() then: - 1 * mockTelemetryCollector.onInjectionSkipped("3") + 1 * mockTelemetryCollector.onInjectionSkipped(SERVLET_VERSION) 1 * mockResponse.getWriter() } @@ -66,7 +67,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed("3", "none") + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, null) } void 'getWriter exception reports failure'() { @@ -80,7 +81,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed("3", "none") + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, null) } void 'setHeader with Content-Security-Policy reports CSP detected'() { @@ -88,7 +89,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.setHeader("Content-Security-Policy", "test") } @@ -97,7 +98,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.addHeader("Content-Security-Policy", "test") } @@ -106,7 +107,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.setHeader("X-Content-Security-Policy", "test") } @@ -115,7 +116,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("3") + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } @@ -127,7 +128,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def marker = "".getBytes("UTF-8") def contentToInject = "".getBytes("UTF-8") def onBytesWritten = { bytes -> - mockTelemetryCollector.onInjectionResponseSize("3", bytes) + mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, bytes) } def wrappedStream = new datadog.trace.instrumentation.servlet3.WrappedServletOutputStream( downstream, marker, contentToInject, null, onBytesWritten) @@ -138,7 +139,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrappedStream.close() then: - 1 * mockTelemetryCollector.onInjectionResponseSize("3", 11) + 1 * mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, 11) } void 'response sizes are reported by the InjectingPipeOutputStream callback'() { @@ -186,7 +187,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() // report timing when injection is successful then: - 1 * mockTelemetryCollector.onInjectionSucceed("3") - 1 * mockTelemetryCollector.onInjectionTime("3", { it > 0 }) + 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) + 1 * mockTelemetryCollector.onInjectionTime(SERVLET_VERSION, { it > 0 }) } } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 2d8013676e7..df47f79c718 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -16,7 +16,9 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private PrintWriter printWriter; private boolean shouldInject = true; private long injectionStartTime = -1; - private String contentEncoding = "none"; + private String contentEncoding = null; + + private static final String SERVLET_VERSION = "5"; public RumHttpServletResponseWrapper(HttpServletResponse response) { super(response); @@ -29,7 +31,7 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped("5"); + RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); return super.getOutputStream(); } // start timing injection @@ -47,10 +49,12 @@ public ServletOutputStream getOutputStream() throws IOException { rumInjector.getMarkerBytes(encoding), rumInjector.getSnippetBytes(encoding), this::onInjected, - bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("5", bytes)); + bytes -> + RumInjector.getTelemetryCollector() + .onInjectionResponseSize(SERVLET_VERSION, bytes)); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed("5", contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); throw e; } return outputStream; @@ -62,7 +66,7 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped("5"); + RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); return super.getWriter(); } // start timing injection @@ -76,11 +80,13 @@ public PrintWriter getWriter() throws IOException { rumInjector.getMarkerChars(), rumInjector.getSnippetChars(), this::onInjected, - bytes -> RumInjector.getTelemetryCollector().onInjectionResponseSize("5", bytes)); + bytes -> + RumInjector.getTelemetryCollector() + .onInjectionResponseSize(SERVLET_VERSION, bytes)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed("5", contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); throw e; } @@ -92,7 +98,7 @@ public void setHeader(String name, String value) { if (name != null) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("5"); + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } @@ -105,7 +111,7 @@ public void addHeader(String name, String value) { if (name != null) { String lowerName = name.toLowerCase(); if (lowerName.startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected("5"); + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); } else if (lowerName.contains("content-encoding")) { this.contentEncoding = value; } @@ -147,13 +153,13 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.getTelemetryCollector().onInjectionSucceed("5"); + RumInjector.getTelemetryCollector().onInjectionSucceed(SERVLET_VERSION); // calculate total injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime("5", milliseconds); + RumInjector.getTelemetryCollector().onInjectionTime(SERVLET_VERSION, milliseconds); injectionStartTime = -1; } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index d878df85f0d..ab91df732a3 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -7,6 +7,7 @@ import spock.lang.Subject import jakarta.servlet.http.HttpServletResponse class RumHttpServletResponseWrapperTest extends AgentTestRunner { + private static final String SERVLET_VERSION = "5" def mockResponse = Mock(HttpServletResponse) def mockTelemetryCollector = Mock(RumTelemetryCollector) @@ -28,7 +29,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() then: - 1 * mockTelemetryCollector.onInjectionSucceed("5") + 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) } void 'getOutputStream with non-HTML content reports skipped'() { @@ -39,7 +40,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getOutputStream() then: - 1 * mockTelemetryCollector.onInjectionSkipped("5") + 1 * mockTelemetryCollector.onInjectionSkipped(SERVLET_VERSION) 1 * mockResponse.getOutputStream() } @@ -51,7 +52,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.getWriter() then: - 1 * mockTelemetryCollector.onInjectionSkipped("5") + 1 * mockTelemetryCollector.onInjectionSkipped(SERVLET_VERSION) 1 * mockResponse.getWriter() } @@ -66,7 +67,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed("5", "none") + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, null) } void 'getWriter exception reports failure'() { @@ -80,7 +81,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } catch (IOException ignored) {} then: - 1 * mockTelemetryCollector.onInjectionFailed("5", "none") + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, null) } void 'setHeader with Content-Security-Policy reports CSP detected'() { @@ -88,7 +89,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.setHeader("Content-Security-Policy", "test") } @@ -97,7 +98,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("Content-Security-Policy", "test") then: - 1 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") + 1 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.addHeader("Content-Security-Policy", "test") } @@ -106,7 +107,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.setHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.setHeader("X-Content-Security-Policy", "test") } @@ -115,7 +116,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.addHeader("X-Content-Security-Policy", "test") then: - 0 * mockTelemetryCollector.onContentSecurityPolicyDetected("5") + 0 * mockTelemetryCollector.onContentSecurityPolicyDetected(SERVLET_VERSION) 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } @@ -127,7 +128,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def marker = "".getBytes("UTF-8") def contentToInject = "".getBytes("UTF-8") def onBytesWritten = { bytes -> - mockTelemetryCollector.onInjectionResponseSize("5", bytes) + mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, bytes) } def wrappedStream = new datadog.trace.instrumentation.servlet5.WrappedServletOutputStream( downstream, marker, contentToInject, null, onBytesWritten) @@ -138,7 +139,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrappedStream.close() then: - 1 * mockTelemetryCollector.onInjectionResponseSize("5", 11) + 1 * mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, 11) } void 'response sizes are reported by the InjectingPipeOutputStream callback'() { @@ -186,7 +187,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { wrapper.onInjected() // report timing when injection is successful then: - 1 * mockTelemetryCollector.onInjectionSucceed("5") - 1 * mockTelemetryCollector.onInjectionTime("5", { it > 0 }) + 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) + 1 * mockTelemetryCollector.onInjectionTime(SERVLET_VERSION, { it > 0 }) } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java index f813c019030..343c9450aa6 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -150,7 +150,7 @@ public static void shutdownTelemetry() { } /** - * Sets the telemetry collector. + * Sets the telemetry collector. This is used for testing purposes only. * * @param collector The telemetry collector to set or {@code null} to reset to NO_OP. */ diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 17fcdb66929..8f7b32aa8bd 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -1,12 +1,17 @@ package datadog.trace.api.rum; +import datadog.trace.api.Config; import datadog.trace.api.StatsDClient; import java.util.concurrent.atomic.AtomicLong; -// This class implements the RumTelemetryCollector interface, which is used to collect telemetry -// from the RumInjector. Metrics are then reported via StatsDClient with tagging. See: -// https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/common_metrics.json -// for common metrics and tags. +/** + * This class implements the RumTelemetryCollector interface, which is used to collect telemetry + * from the RumInjector. Metrics are then reported via StatsDClient with tagging. + * + * @see common + * metrics and tags + */ public class RumInjectorMetrics implements RumTelemetryCollector { // Use static tags for common combinations so that we don't have to build them for each metric private static final String[] CSP_SERVLET3_TAGS = @@ -72,15 +77,10 @@ public RumInjectorMetrics(final StatsDClient statsd) { // Get RUM config values (applicationId and remoteConfigUsed) for tagging RumInjector rumInjector = RumInjector.get(); - if (rumInjector.isEnabled()) { - RumInjectorConfig injectorConfig = datadog.trace.api.Config.get().getRumInjectorConfig(); - if (injectorConfig != null) { - this.applicationId = injectorConfig.applicationId; - this.remoteConfigUsed = injectorConfig.remoteConfigurationId != null ? "true" : "false"; - } else { - this.applicationId = "unknown"; - this.remoteConfigUsed = "false"; - } + RumInjectorConfig injectorConfig = Config.get().getRumInjectorConfig(); + if (rumInjector.isEnabled() && injectorConfig != null) { + this.applicationId = injectorConfig.applicationId; + this.remoteConfigUsed = injectorConfig.remoteConfigurationId != null ? "true" : "false"; } else { this.applicationId = "unknown"; this.remoteConfigUsed = "false"; diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java index 94ced1afa57..74638bdffec 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java @@ -1,7 +1,9 @@ package datadog.trace.api.rum; -// Collect RUM injection telemetry from the RumInjector -// This is implemented by the RumInjectorMetrics class +/** + * Collect RUM injection telemetry from the RumInjector This is implemented by the + * RumInjectorMetrics class + */ public interface RumTelemetryCollector { RumTelemetryCollector NO_OP = diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index cdf7c38727f..13b19d470fd 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -36,7 +36,7 @@ class RumInjectorMetricsTest extends Specification { def "test onInjectionFailed"() { when: metrics.onInjectionFailed("3", "gzip") - metrics.onInjectionFailed("5", "none") + metrics.onInjectionFailed("5", null) then: 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> @@ -49,7 +49,7 @@ class RumInjectorMetricsTest extends Specification { } 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("content_encoding:none") + assert tags.contains("content_encoding:null") assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") assert tags.contains("integration_version:5") @@ -173,7 +173,7 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionSkipped("5") metrics.onInjectionFailed("3", "gzip") metrics.onInjectionSucceed("3") - metrics.onInjectionFailed("5", "none") + metrics.onInjectionFailed("5", null) metrics.onInjectionSucceed("3") metrics.onInjectionSkipped("3") metrics.onContentSecurityPolicyDetected("5") diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy index 3f6215402fa..19c423635a7 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumTelemetryCollectorTest.groovy @@ -9,7 +9,7 @@ class RumTelemetryCollectorTest extends Specification { RumTelemetryCollector.NO_OP.onInjectionSucceed("3") RumTelemetryCollector.NO_OP.onInjectionSucceed("5") RumTelemetryCollector.NO_OP.onInjectionFailed("3", "gzip") - RumTelemetryCollector.NO_OP.onInjectionFailed("5", "none") + RumTelemetryCollector.NO_OP.onInjectionFailed("5", null) RumTelemetryCollector.NO_OP.onInjectionSkipped("3") RumTelemetryCollector.NO_OP.onInjectionSkipped("5") RumTelemetryCollector.NO_OP.onInitializationSucceed() From 3e462d46bb25fb7594128b2ffd85fddfd102ee6f Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Tue, 12 Aug 2025 14:35:42 -0400 Subject: [PATCH 22/31] Fix header retrieval --- .../RumHttpServletResponseWrapper.java | 30 +++++++------- .../RumHttpServletResponseWrapperTest.groovy | 40 +++++++++++++++++++ .../RumHttpServletResponseWrapper.java | 30 +++++++------- .../RumHttpServletResponseWrapperTest.groovy | 40 +++++++++++++++++++ .../trace/api/rum/RumInjectorMetrics.java | 33 ++++++++++----- .../api/rum/RumInjectorMetricsTest.groovy | 2 +- 6 files changed, 136 insertions(+), 39 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index af4ce3234ab..f61fbdb77e7 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -113,28 +113,22 @@ public PrintWriter getWriter() throws IOException { @Override public void setHeader(String name, String value) { - if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); - } else if (lowerName.contains("content-encoding")) { - this.contentEncoding = value; - } - } + checkForContentSecurityPolicy(name); super.setHeader(name, value); } @Override public void addHeader(String name, String value) { - if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { + checkForContentSecurityPolicy(name); + super.addHeader(name, value); + } + + private void checkForContentSecurityPolicy(String name) { + if (name != null && rumInjector.isEnabled()) { + if (name.startsWith("Content-Security-Policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); - } else if (lowerName.contains("content-encoding")) { - this.contentEncoding = value; } } - super.addHeader(name, value); } @Override @@ -156,6 +150,14 @@ public void setContentLengthLong(long len) { } } + @Override + public void setCharacterEncoding(String charset) { + if (charset != null && rumInjector.isEnabled()) { + this.contentEncoding = charset; + } + super.setCharacterEncoding(charset); + } + @Override public void reset() { this.outputStream = null; diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 50135d0ecce..d71a8053e58 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -12,6 +12,16 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def mockResponse = Mock(HttpServletResponse) def mockTelemetryCollector = Mock(RumTelemetryCollector) + // injector needs to be enabled in order to check headers + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("rum.enabled", "true") + injectSysConfig("rum.application.id", "test") + injectSysConfig("rum.client.token", "secret") + injectSysConfig("rum.remote.configuration.id", "12345") + } + @Subject RumHttpServletResponseWrapper wrapper @@ -120,6 +130,36 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } + void 'setCharacterEncoding sets content-encoding tag with value when injection fails'() { + setup: + wrapper.setContentType("text/html") + wrapper.setCharacterEncoding("UTF-8") + mockResponse.getOutputStream() >> { throw new IOException("stream error") } + + when: + try { + wrapper.getOutputStream() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, "UTF-8") + } + + void 'setCharacterEncoding with null does not set content-encoding tag when injection fails'() { + setup: + wrapper.setContentType("text/html") + wrapper.setCharacterEncoding(null) + mockResponse.getOutputStream() >> { throw new IOException("stream error") } + + when: + try { + wrapper.getOutputStream() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, null) + } + // Callback is created in the RumHttpServletResponseWrapper and passed to InjectingPipeOutputStream via WrappedServletOutputStream. // When the stream is closed, the callback is called with the total number of bytes written to the stream. void 'response sizes are reported to the telemetry collector via the WrappedServletOutputStream callback'() { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index df47f79c718..3e65164514e 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -95,28 +95,22 @@ public PrintWriter getWriter() throws IOException { @Override public void setHeader(String name, String value) { - if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); - } else if (lowerName.contains("content-encoding")) { - this.contentEncoding = value; - } - } + checkForContentSecurityPolicy(name); super.setHeader(name, value); } @Override public void addHeader(String name, String value) { - if (name != null) { - String lowerName = name.toLowerCase(); - if (lowerName.startsWith("content-security-policy")) { + checkForContentSecurityPolicy(name); + super.addHeader(name, value); + } + + private void checkForContentSecurityPolicy(String name) { + if (name != null && rumInjector.isEnabled()) { + if (name.startsWith("Content-Security-Policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); - } else if (lowerName.contains("content-encoding")) { - this.contentEncoding = value; } } - super.addHeader(name, value); } @Override @@ -134,6 +128,14 @@ public void setContentLengthLong(long len) { } } + @Override + public void setCharacterEncoding(String charset) { + if (charset != null && rumInjector.isEnabled()) { + this.contentEncoding = charset; + } + super.setCharacterEncoding(charset); + } + @Override public void reset() { this.outputStream = null; diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index ab91df732a3..3502a0c1a9a 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -12,6 +12,16 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def mockResponse = Mock(HttpServletResponse) def mockTelemetryCollector = Mock(RumTelemetryCollector) + // injector needs to be enabled in order to check headers + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("rum.enabled", "true") + injectSysConfig("rum.application.id", "test") + injectSysConfig("rum.client.token", "secret") + injectSysConfig("rum.remote.configuration.id", "12345") + } + @Subject RumHttpServletResponseWrapper wrapper @@ -120,6 +130,36 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } + void 'setCharacterEncoding sets content-encoding tag with value when injection fails'() { + setup: + wrapper.setContentType("text/html") + wrapper.setCharacterEncoding("UTF-8") + mockResponse.getOutputStream() >> { throw new IOException("stream error") } + + when: + try { + wrapper.getOutputStream() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, "UTF-8") + } + + void 'setCharacterEncoding with null does not set content-encoding tag when injection fails'() { + setup: + wrapper.setContentType("text/html") + wrapper.setCharacterEncoding(null) + mockResponse.getOutputStream() >> { throw new IOException("stream error") } + + when: + try { + wrapper.getOutputStream() + } catch (IOException ignored) {} + + then: + 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, null) + } + // Callback is created in the RumHttpServletResponseWrapper and passed to InjectingPipeOutputStream via WrappedServletOutputStream. // When the stream is closed, the callback is called with the total number of bytes written to the stream. void 'response sizes are reported to the telemetry collector via the WrappedServletOutputStream callback'() { diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 8f7b32aa8bd..063b930900c 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -107,16 +107,29 @@ public void onInjectionSucceed(String integrationVersion) { public void onInjectionFailed(String integrationVersion, String contentEncoding) { injectionFailed.incrementAndGet(); - String[] tags = - new String[] { - "application_id:" + applicationId, - "content_encoding:" + contentEncoding, - "injector_version:0.1.0", - "integration_name:servlet", - "integration_version:" + integrationVersion, - "reason:failed_to_return_response_wrapper", - "remote_config_used:" + remoteConfigUsed - }; + String[] tags; + if (contentEncoding != null) { + tags = + new String[] { + "application_id:" + applicationId, + "content_encoding:" + contentEncoding, + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:" + integrationVersion, + "reason:failed_to_return_response_wrapper", + "remote_config_used:" + remoteConfigUsed + }; + } else { + tags = + new String[] { + "application_id:" + applicationId, + "injector_version:0.1.0", + "integration_name:servlet", + "integration_version:" + integrationVersion, + "reason:failed_to_return_response_wrapper", + "remote_config_used:" + remoteConfigUsed + }; + } statsd.count("rum.injection.failed", 1, tags); } diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 13b19d470fd..9be8ab7a3a0 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -49,7 +49,7 @@ class RumInjectorMetricsTest extends Specification { } 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("content_encoding:null") + assert !tags.any { it.startsWith("content_encoding:") } assert tags.contains("injector_version:0.1.0") assert tags.contains("integration_name:servlet") assert tags.contains("integration_version:5") From b1ab8712ba873a92d259f49b51ec32875213ad86 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Tue, 12 Aug 2025 18:10:04 -0400 Subject: [PATCH 23/31] Add lots of improvements from review comments --- .../InjectingPipeOutputStreamBenchmark.java | 3 +- .../buffer/InjectingPipeOutputStream.java | 14 +++ .../buffer/InjectingPipeWriter.java | 14 +++ .../InjectingPipeOutputStreamTest.groovy | 91 +++++++++---------- .../buffer/InjectingPipeWriterTest.groovy | 61 +++++-------- .../RumHttpServletResponseWrapperTest.groovy | 27 ++++-- .../RumHttpServletResponseWrapperTest.groovy | 27 ++++-- .../trace/api/rum/RumInjectorMetrics.java | 29 ++---- .../api/rum/RumInjectorMetricsTest.groovy | 71 ++++----------- 9 files changed, 152 insertions(+), 185 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java index bec95c8a938..8b90b4678a7 100644 --- a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java +++ b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java @@ -51,8 +51,7 @@ public class InjectingPipeOutputStreamBenchmark { public void withPipe() throws Exception { try (final PrintWriter out = new PrintWriter( - new InjectingPipeOutputStream( - new ByteArrayOutputStream(), marker, content, null, null))) { + new InjectingPipeOutputStream(new ByteArrayOutputStream(), marker, content, null))) { htmlContent.forEach(out::println); } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index b1f06d3c1f9..c146e89cdf2 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -27,6 +27,20 @@ public class InjectingPipeOutputStream extends OutputStream { private final LongConsumer onBytesWritten; private long bytesWritten = 0; + /** + * @param downstream the delegate output stream + * @param marker the marker to find in the stream. Must at least be one byte. + * @param contentToInject the content to inject once before the marker if found. + * @param onContentInjected callback called when and if the content is injected. + */ + public InjectingPipeOutputStream( + final OutputStream downstream, + final byte[] marker, + final byte[] contentToInject, + final Runnable onContentInjected) { + this(downstream, marker, contentToInject, onContentInjected, null); + } + /** * @param downstream the delegate output stream * @param marker the marker to find in the stream. Must at least be one byte. diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index adea204e054..c4e3fd4dbdc 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -27,6 +27,20 @@ public class InjectingPipeWriter extends Writer { private final LongConsumer onBytesWritten; private long bytesWritten = 0; + /** + * @param downstream the delegate writer + * @param marker the marker to find in the stream. Must at least be one char. + * @param contentToInject the content to inject once before the marker if found. + * @param onContentInjected callback called when and if the content is injected. + */ + public InjectingPipeWriter( + final Writer downstream, + final char[] marker, + final char[] contentToInject, + final Runnable onContentInjected) { + this(downstream, marker, contentToInject, onContentInjected, null); + } + /** * @param downstream the delegate writer * @param marker the marker to find in the stream. Must at least be one char. diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index 451727b0159..d78266a9157 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -3,6 +3,9 @@ package datadog.trace.bootstrap.instrumentation.buffer import datadog.trace.test.util.DDSpecification class InjectingPipeOutputStreamTest extends DDSpecification { + static final byte[] MARKER_BYTES = "".getBytes("UTF-8") + static final byte[] CONTEXT_BYTES = "".getBytes("UTF-8") + static class GlitchedOutputStream extends FilterOutputStream { int glitchesPos int count @@ -33,10 +36,18 @@ class InjectingPipeOutputStreamTest extends DDSpecification { } } + static class Counter { + int value = 0 + + def incr(long count) { + this.value += count + } + } + def 'should filter a buffer and inject if found #found'() { setup: def downstream = new ByteArrayOutputStream() - def piped = new OutputStreamWriter(new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null, null), + def piped = new OutputStreamWriter(new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null), "UTF-8") when: try (def closeme = piped) { @@ -55,7 +66,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { setup: def baos = new ByteArrayOutputStream() def downstream = new GlitchedOutputStream(baos, glichesAt) - def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null, null) + def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null) when: try { for (String line : body) { @@ -90,91 +101,71 @@ class InjectingPipeOutputStreamTest extends DDSpecification { def 'should count bytes correctly when writing byte arrays'() { setup: + def testBytes = "test content".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + def counter = new Counter() + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }) when: - piped.write("test content".getBytes("UTF-8")) + piped.write(testBytes) piped.close() then: - bytesWritten.size() == 1 - bytesWritten[0] == 12 - downstream.toByteArray() == "test content".getBytes("UTF-8") + counter.value == 12 + downstream.toByteArray() == testBytes } def 'should count bytes correctly when writing bytes individually'() { setup: + def testBytes = "test".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + def counter = new Counter() + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }) when: - def bytes = "test".getBytes("UTF-8") - for (int i = 0; i < bytes.length; i++) { - piped.write((int) bytes[i]) + for (int i = 0; i < testBytes.length; i++) { + piped.write((int) testBytes[i]) } piped.close() then: - bytesWritten.size() == 1 - bytesWritten[0] == 4 - downstream.toByteArray() == "test".getBytes("UTF-8") + counter.value == 4 + downstream.toByteArray() == testBytes } def 'should count bytes correctly with multiple writes'() { setup: + def part1 = "test".getBytes("UTF-8") + def part2 = " ".getBytes("UTF-8") + def part3 = "content".getBytes("UTF-8") + def testBytes = "test content".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) + def counter = new Counter() + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }) when: - piped.write("test".getBytes("UTF-8")) - piped.write(" ".getBytes("UTF-8")) - piped.write("content".getBytes("UTF-8")) + piped.write(part1) + piped.write(part2) + piped.write(part3) piped.close() then: - bytesWritten.size() == 1 - bytesWritten[0] == 12 - downstream.toByteArray() == "test content".getBytes("UTF-8") + counter.value == 12 + downstream.toByteArray() == testBytes } def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: + def testBytes = "test content".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() - def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, null) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, null) when: - piped.write("test content".getBytes("UTF-8")) + piped.write(testBytes) piped.close() then: noExceptionThrown() - downstream.toByteArray() == "test content".getBytes("UTF-8") - } - - def 'should reset byte count after close'() { - setup: - def downstream = new ByteArrayOutputStream() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeOutputStream(downstream, "".getBytes("UTF-8"), "".getBytes("UTF-8"), null, onBytesWritten) - - when: - piped.write("test".getBytes("UTF-8")) - piped.close() - - piped.write("content".getBytes("UTF-8")) - piped.close() - - then: - bytesWritten.size() == 2 - bytesWritten[0] == 4 - bytesWritten[1] == 7 + downstream.toByteArray() == testBytes } } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index 94909cac47d..5cdc681affc 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -3,6 +3,9 @@ package datadog.trace.bootstrap.instrumentation.buffer import datadog.trace.test.util.DDSpecification class InjectingPipeWriterTest extends DDSpecification { + static final char[] MARKER_CHARS = "".toCharArray() + static final char[] CONTEXT_CHARS = "".toCharArray() + static class GlitchedWriter extends FilterWriter { int glitchesPos int count @@ -33,10 +36,18 @@ class InjectingPipeWriterTest extends DDSpecification { } } + static class Counter { + int value = 0 + + def incr(long count) { + this.value += count + } + } + def 'should filter a buffer and inject if found #found using write'() { setup: def downstream = new StringWriter() - def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null, null)) + def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null)) when: try (def closeme = piped) { piped.write(body) @@ -53,7 +64,7 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should filter a buffer and inject if found #found using append'() { setup: def downstream = new StringWriter() - def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null, null)) + def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null)) when: try (def closeme = piped) { piped.append(body) @@ -107,26 +118,23 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should count bytes correctly when writing characters'() { setup: def downstream = new StringWriter() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + def counter = new Counter() + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }) when: piped.write("test content".toCharArray()) piped.close() then: - bytesWritten.size() == 1 - bytesWritten[0] == 12 + counter.value == 12 downstream.toString() == "test content" } def 'should count bytes correctly when writing characters individually'() { setup: def downstream = new StringWriter() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + def counter = new Counter() + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }) when: def content = "test" @@ -136,17 +144,15 @@ class InjectingPipeWriterTest extends DDSpecification { piped.close() then: - bytesWritten.size() == 1 - bytesWritten[0] == 4 + counter.value == 4 downstream.toString() == "test" } def 'should count bytes correctly with multiple writes'() { setup: def downstream = new StringWriter() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) + def counter = new Counter() + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }) when: piped.write("test".toCharArray()) @@ -155,15 +161,14 @@ class InjectingPipeWriterTest extends DDSpecification { piped.close() then: - bytesWritten.size() == 1 - bytesWritten[0] == 12 + counter.value == 12 downstream.toString() == "test content" } def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def downstream = new StringWriter() - def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, null) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, null) when: piped.write("test content".toCharArray()) @@ -173,24 +178,4 @@ class InjectingPipeWriterTest extends DDSpecification { noExceptionThrown() downstream.toString() == "test content" } - - def 'should reset byte count after close'() { - setup: - def downstream = new StringWriter() - def bytesWritten = [] - def onBytesWritten = { long bytes -> bytesWritten.add(bytes) } - def piped = new InjectingPipeWriter(downstream, "".toCharArray(), "".toCharArray(), null, onBytesWritten) - - when: - piped.write("test".toCharArray()) - piped.close() - - piped.write("content".toCharArray()) - piped.close() - - then: - bytesWritten.size() == 2 - bytesWritten[0] == 4 - bytesWritten[1] == 7 - } } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index d71a8053e58..94d8e07552c 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -1,9 +1,13 @@ import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.rum.RumInjector import datadog.trace.api.rum.RumTelemetryCollector +import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream +import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter import datadog.trace.instrumentation.servlet3.RumHttpServletResponseWrapper +import datadog.trace.instrumentation.servlet3.WrappedServletOutputStream import spock.lang.Subject +import java.util.function.LongConsumer import javax.servlet.http.HttpServletResponse class RumHttpServletResponseWrapperTest extends AgentTestRunner { @@ -130,7 +134,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } - void 'setCharacterEncoding sets content-encoding tag with value when injection fails'() { + void 'setCharacterEncoding reports the content-encoding tag with value when injection fails'() { setup: wrapper.setContentType("text/html") wrapper.setCharacterEncoding("UTF-8") @@ -145,7 +149,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, "UTF-8") } - void 'setCharacterEncoding with null does not set content-encoding tag when injection fails'() { + void 'setCharacterEncoding reports the content-encoding tag with null when injection fails'() { setup: wrapper.setContentType("text/html") wrapper.setCharacterEncoding(null) @@ -170,7 +174,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def onBytesWritten = { bytes -> mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, bytes) } - def wrappedStream = new datadog.trace.instrumentation.servlet3.WrappedServletOutputStream( + def wrappedStream = new WrappedServletOutputStream( downstream, marker, contentToInject, null, onBytesWritten) when: @@ -187,8 +191,8 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def downstream = Mock(java.io.OutputStream) def marker = "".getBytes("UTF-8") def contentToInject = "".getBytes("UTF-8") - def onBytesWritten = Mock(java.util.function.LongConsumer) - def stream = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream( + def onBytesWritten = Mock(LongConsumer) + def stream = new InjectingPipeOutputStream( downstream, marker, contentToInject, null, onBytesWritten) when: @@ -205,8 +209,8 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def downstream = Mock(java.io.Writer) def marker = "".toCharArray() def contentToInject = "".toCharArray() - def onBytesWritten = Mock(java.util.function.LongConsumer) - def writer = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter( + def onBytesWritten = Mock(LongConsumer) + def writer = new InjectingPipeWriter( downstream, marker, contentToInject, null, onBytesWritten) when: @@ -220,11 +224,14 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { void 'injection timing is reported when injection is successful'() { setup: - // set the injection start time to simulate timing - wrapper.@injectionStartTime = System.nanoTime() - 2_000_000L + wrapper.setContentType("text/html") + def mockWriter = Mock(java.io.PrintWriter) + mockResponse.getWriter() >> mockWriter when: - wrapper.onInjected() // report timing when injection is successful + wrapper.getWriter() + Thread.sleep(1) // ensure measurable time passes + wrapper.onInjected() then: 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 3502a0c1a9a..d0a33eb5c11 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -1,9 +1,13 @@ import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.rum.RumInjector import datadog.trace.api.rum.RumTelemetryCollector +import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream +import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter import datadog.trace.instrumentation.servlet5.RumHttpServletResponseWrapper +import datadog.trace.instrumentation.servlet5.WrappedServletOutputStream import spock.lang.Subject +import java.util.function.LongConsumer import jakarta.servlet.http.HttpServletResponse class RumHttpServletResponseWrapperTest extends AgentTestRunner { @@ -130,7 +134,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockResponse.addHeader("X-Content-Security-Policy", "test") } - void 'setCharacterEncoding sets content-encoding tag with value when injection fails'() { + void 'setCharacterEncoding reports the content-encoding tag with value when injection fails'() { setup: wrapper.setContentType("text/html") wrapper.setCharacterEncoding("UTF-8") @@ -145,7 +149,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * mockTelemetryCollector.onInjectionFailed(SERVLET_VERSION, "UTF-8") } - void 'setCharacterEncoding with null does not set content-encoding tag when injection fails'() { + void 'setCharacterEncoding reports the content-encoding tag with null when injection fails'() { setup: wrapper.setContentType("text/html") wrapper.setCharacterEncoding(null) @@ -170,7 +174,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def onBytesWritten = { bytes -> mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, bytes) } - def wrappedStream = new datadog.trace.instrumentation.servlet5.WrappedServletOutputStream( + def wrappedStream = new WrappedServletOutputStream( downstream, marker, contentToInject, null, onBytesWritten) when: @@ -187,8 +191,8 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def downstream = Mock(java.io.OutputStream) def marker = "".getBytes("UTF-8") def contentToInject = "".getBytes("UTF-8") - def onBytesWritten = Mock(java.util.function.LongConsumer) - def stream = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream( + def onBytesWritten = Mock(LongConsumer) + def stream = new InjectingPipeOutputStream( downstream, marker, contentToInject, null, onBytesWritten) when: @@ -205,8 +209,8 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def downstream = Mock(java.io.Writer) def marker = "".toCharArray() def contentToInject = "".toCharArray() - def onBytesWritten = Mock(java.util.function.LongConsumer) - def writer = new datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter( + def onBytesWritten = Mock(LongConsumer) + def writer = new InjectingPipeWriter( downstream, marker, contentToInject, null, onBytesWritten) when: @@ -220,11 +224,14 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { void 'injection timing is reported when injection is successful'() { setup: - // set the injection start time to simulate timing - wrapper.@injectionStartTime = System.nanoTime() - 2_000_000L + wrapper.setContentType("text/html") + def mockWriter = Mock(java.io.PrintWriter) + mockResponse.getWriter() >> mockWriter when: - wrapper.onInjected() // report timing when injection is successful + wrapper.getWriter() + Thread.sleep(1) // ensure measurable time passes + wrapper.onInjected() then: 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 063b930900c..7b67dff6bfd 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -14,9 +14,9 @@ */ public class RumInjectorMetrics implements RumTelemetryCollector { // Use static tags for common combinations so that we don't have to build them for each metric + // Note that injector_version tags are not included because we do not use the rust injector private static final String[] CSP_SERVLET3_TAGS = new String[] { - "injector_version:0.1.0", "integration_name:servlet", "integration_version:3", "kind:header", @@ -26,7 +26,6 @@ public class RumInjectorMetrics implements RumTelemetryCollector { private static final String[] CSP_SERVLET5_TAGS = new String[] { - "injector_version:0.1.0", "integration_name:servlet", "integration_version:5", "kind:header", @@ -35,31 +34,19 @@ public class RumInjectorMetrics implements RumTelemetryCollector { }; private static final String[] INIT_TAGS = - new String[] { - "injector_version:0.1.0", "integration_name:servlet", "integration_version:3,5" - }; + new String[] {"integration_name:servlet", "integration_version:3,5"}; private static final String[] TIME_SERVLET3_TAGS = - new String[] {"injector_version:0.1.0", "integration_name:servlet", "integration_version:3"}; + new String[] {"integration_name:servlet", "integration_version:3"}; private static final String[] TIME_SERVLET5_TAGS = - new String[] {"injector_version:0.1.0", "integration_name:servlet", "integration_version:5"}; + new String[] {"integration_name:servlet", "integration_version:5"}; private static final String[] RESPONSE_SERVLET3_TAGS = - new String[] { - "injector_version:0.1.0", - "integration_name:servlet", - "integration_version:3", - "response_kind:header" - }; + new String[] {"integration_name:servlet", "integration_version:3", "response_kind:header"}; private static final String[] RESPONSE_SERVLET5_TAGS = - new String[] { - "injector_version:0.1.0", - "integration_name:servlet", - "integration_version:5", - "response_kind:header" - }; + new String[] {"integration_name:servlet", "integration_version:5", "response_kind:header"}; private final AtomicLong injectionSucceed = new AtomicLong(); private final AtomicLong injectionFailed = new AtomicLong(); @@ -94,7 +81,6 @@ public void onInjectionSucceed(String integrationVersion) { String[] tags = new String[] { "application_id:" + applicationId, - "injector_version:0.1.0", "integration_name:servlet", "integration_version:" + integrationVersion, "remote_config_used:" + remoteConfigUsed @@ -113,7 +99,6 @@ public void onInjectionFailed(String integrationVersion, String contentEncoding) new String[] { "application_id:" + applicationId, "content_encoding:" + contentEncoding, - "injector_version:0.1.0", "integration_name:servlet", "integration_version:" + integrationVersion, "reason:failed_to_return_response_wrapper", @@ -123,7 +108,6 @@ public void onInjectionFailed(String integrationVersion, String contentEncoding) tags = new String[] { "application_id:" + applicationId, - "injector_version:0.1.0", "integration_name:servlet", "integration_version:" + integrationVersion, "reason:failed_to_return_response_wrapper", @@ -141,7 +125,6 @@ public void onInjectionSkipped(String integrationVersion) { String[] tags = new String[] { "application_id:" + applicationId, - "injector_version:0.1.0", "integration_name:servlet", "integration_version:" + integrationVersion, "reason:should_not_inject", diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 9be8ab7a3a0..286cfcfb4db 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -10,6 +10,12 @@ class RumInjectorMetricsTest extends Specification { @Subject def metrics = new RumInjectorMetrics(statsD) + void assertTags(String[] args, String... expectedTags) { + expectedTags.each { expectedTag -> + assert args.contains(expectedTag), "Expected tag '$expectedTag' not found in tags: ${args as List}" + } + } + // Note: application_id and remote_config_used are dynamic runtime values that depend on // the RUM configuration state, so we do not test them here. def "test onInjectionSucceed"() { @@ -20,15 +26,11 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3") + assertTags(tags, "integration_name:servlet", "integration_version:3") } 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:5") + assertTags(tags, "integration_name:servlet", "integration_version:5") } 0 * _ } @@ -41,19 +43,12 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("content_encoding:gzip") - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3") - assert tags.contains("reason:failed_to_return_response_wrapper") + assertTags(tags, "content_encoding:gzip", "integration_name:servlet", "integration_version:3", "reason:failed_to_return_response_wrapper") } 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> def tags = args[2] as String[] assert !tags.any { it.startsWith("content_encoding:") } - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:5") - assert tags.contains("reason:failed_to_return_response_wrapper") + assertTags(tags, "integration_name:servlet", "integration_version:5", "reason:failed_to_return_response_wrapper") } 0 * _ } @@ -66,17 +61,11 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3") - assert tags.contains("reason:should_not_inject") + assertTags(tags, "integration_name:servlet", "integration_version:3", "reason:should_not_inject") } 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:5") - assert tags.contains("reason:should_not_inject") + assertTags(tags, "integration_name:servlet", "integration_version:5", "reason:should_not_inject") } 0 * _ } @@ -89,21 +78,11 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3") - assert tags.contains("kind:header") - assert tags.contains("reason:csp_header_found") - assert tags.contains("status:seen") + assertTags(tags, "integration_name:servlet", "integration_version:3", "kind:header", "reason:csp_header_found", "status:seen") } 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:5") - assert tags.contains("kind:header") - assert tags.contains("reason:csp_header_found") - assert tags.contains("status:seen") + assertTags(tags, "integration_name:servlet", "integration_version:5", "kind:header", "reason:csp_header_found", "status:seen") } 0 * _ } @@ -115,9 +94,7 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.initialization.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3,5") + assertTags(tags, "integration_name:servlet", "integration_version:3,5") } 0 * _ } @@ -130,17 +107,11 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.distribution('rum.injection.response.bytes', 256, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3") - assert tags.contains("response_kind:header") + assertTags(tags, "integration_name:servlet", "integration_version:3", "response_kind:header") } 1 * statsD.distribution('rum.injection.response.bytes', 512, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:5") - assert tags.contains("response_kind:header") + assertTags(tags, "integration_name:servlet", "integration_version:5", "response_kind:header") } 0 * _ } @@ -153,15 +124,11 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.distribution('rum.injection.ms', 5L, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:5") + assertTags(tags, "integration_name:servlet", "integration_version:5") } 1 * statsD.distribution('rum.injection.ms', 10L, _) >> { args -> def tags = args[2] as String[] - assert tags.contains("injector_version:0.1.0") - assert tags.contains("integration_name:servlet") - assert tags.contains("integration_version:3") + assertTags(tags, "integration_name:servlet", "integration_version:3") } 0 * _ } From 93bd0a4216b5085c496741316e80554065b111bc Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Wed, 13 Aug 2025 12:55:59 -0400 Subject: [PATCH 24/31] Fix constructors and address review comment --- .../buffer/InjectingPipeOutputStreamBenchmark.java | 2 +- .../buffer/InjectingPipeOutputStream.java | 13 +++++++------ .../instrumentation/buffer/InjectingPipeWriter.java | 13 +++++++------ .../buffer/InjectingPipeOutputStreamTest.groovy | 6 +++--- .../buffer/InjectingPipeWriterTest.groovy | 8 ++++---- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java index 8b90b4678a7..8f52f13ee86 100644 --- a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java +++ b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java @@ -51,7 +51,7 @@ public class InjectingPipeOutputStreamBenchmark { public void withPipe() throws Exception { try (final PrintWriter out = new PrintWriter( - new InjectingPipeOutputStream(new ByteArrayOutputStream(), marker, content, null))) { + new InjectingPipeOutputStream(new ByteArrayOutputStream(), marker, content))) { htmlContent.forEach(out::println); } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index c146e89cdf2..58e601c76c9 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -28,20 +28,21 @@ public class InjectingPipeOutputStream extends OutputStream { private long bytesWritten = 0; /** + * This constructor is typically used for testing where we care about the logic and not the + * telemetry. + * * @param downstream the delegate output stream * @param marker the marker to find in the stream. Must at least be one byte. * @param contentToInject the content to inject once before the marker if found. - * @param onContentInjected callback called when and if the content is injected. */ public InjectingPipeOutputStream( - final OutputStream downstream, - final byte[] marker, - final byte[] contentToInject, - final Runnable onContentInjected) { - this(downstream, marker, contentToInject, onContentInjected, null); + final OutputStream downstream, final byte[] marker, final byte[] contentToInject) { + this(downstream, marker, contentToInject, null, null); } /** + * This constructor contains the full set of parameters. + * * @param downstream the delegate output stream * @param marker the marker to find in the stream. Must at least be one byte. * @param contentToInject the content to inject once before the marker if found. diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index c4e3fd4dbdc..9629afa84c0 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -28,20 +28,21 @@ public class InjectingPipeWriter extends Writer { private long bytesWritten = 0; /** + * This constructor is typically used for testing where we care about the logic and not the + * telemetry. + * * @param downstream the delegate writer * @param marker the marker to find in the stream. Must at least be one char. * @param contentToInject the content to inject once before the marker if found. - * @param onContentInjected callback called when and if the content is injected. */ public InjectingPipeWriter( - final Writer downstream, - final char[] marker, - final char[] contentToInject, - final Runnable onContentInjected) { - this(downstream, marker, contentToInject, onContentInjected, null); + final Writer downstream, final char[] marker, final char[] contentToInject) { + this(downstream, marker, contentToInject, null, null); } /** + * This constructor contains the full set of parameters. + * * @param downstream the delegate writer * @param marker the marker to find in the stream. Must at least be one char. * @param contentToInject the content to inject once before the marker if found. diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index d78266a9157..41d275d37c0 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -47,7 +47,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { def 'should filter a buffer and inject if found #found'() { setup: def downstream = new ByteArrayOutputStream() - def piped = new OutputStreamWriter(new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null), + def piped = new OutputStreamWriter(new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8")), "UTF-8") when: try (def closeme = piped) { @@ -66,7 +66,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { setup: def baos = new ByteArrayOutputStream() def downstream = new GlitchedOutputStream(baos, glichesAt) - def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8"), null) + def piped = new InjectingPipeOutputStream(downstream, marker.getBytes("UTF-8"), contentToInject.getBytes("UTF-8")) when: try { for (String line : body) { @@ -158,7 +158,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { setup: def testBytes = "test content".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() - def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, null) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES) when: piped.write(testBytes) diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index 5cdc681affc..049922045f0 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -47,7 +47,7 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should filter a buffer and inject if found #found using write'() { setup: def downstream = new StringWriter() - def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null)) + def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray())) when: try (def closeme = piped) { piped.write(body) @@ -64,7 +64,7 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should filter a buffer and inject if found #found using append'() { setup: def downstream = new StringWriter() - def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null)) + def piped = new PrintWriter(new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray())) when: try (def closeme = piped) { piped.append(body) @@ -82,7 +82,7 @@ class InjectingPipeWriterTest extends DDSpecification { setup: def writer = new StringWriter() def downstream = new GlitchedWriter(writer, glichesAt) - def piped = new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray(), null, null) + def piped = new InjectingPipeWriter(downstream, marker.toCharArray(), contentToInject.toCharArray()) when: try { for (String line : body) { @@ -168,7 +168,7 @@ class InjectingPipeWriterTest extends DDSpecification { def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def downstream = new StringWriter() - def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, null) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS) when: piped.write("test content".toCharArray()) From b20d6c46fda3ea83f5c508dbe0cd654731f4d934 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Wed, 13 Aug 2025 15:25:46 -0400 Subject: [PATCH 25/31] Clarify bytes written and address review comment --- .../buffer/InjectingPipeOutputStream.java | 15 +++++++++------ .../buffer/InjectingPipeWriter.java | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index 58e601c76c9..686170ea380 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -127,14 +127,16 @@ public void write(byte[] array, int off, int len) throws IOException { // we have a full match. just write everything filter = false; drain(); - downstream.write(array, off, idx); - bytesWritten += idx; + int bytesToWrite = idx; + downstream.write(array, off, bytesToWrite); + bytesWritten += bytesToWrite; downstream.write(contentToInject); if (onContentInjected != null) { onContentInjected.run(); } - downstream.write(array, off + idx, len - idx); - bytesWritten += (len - idx); + bytesToWrite = len - idx; + downstream.write(array, off + idx, bytesToWrite); + bytesWritten += bytesToWrite; } else { // we don't have a full match. write everything in a bulk except the lookbehind buffer // sequentially @@ -146,8 +148,9 @@ public void write(byte[] array, int off, int len) throws IOException { // will be reset if no errors after the following write filter = false; - downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold); - bytesWritten += (len - bulkWriteThreshold); + int bytesToWrite = len - bulkWriteThreshold; + downstream.write(array, off + marker.length - 1, bytesToWrite); + bytesWritten += bytesToWrite; filter = wasFiltering; for (int i = len - marker.length + 1; i < len; i++) { write(array[i]); diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index 9629afa84c0..83969617a84 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -127,14 +127,16 @@ public void write(char[] array, int off, int len) throws IOException { // we have a full match. just write everything filter = false; drain(); - downstream.write(array, off, idx); - bytesWritten += idx; + int bytesToWrite = idx; + downstream.write(array, off, bytesToWrite); + bytesWritten += bytesToWrite; downstream.write(contentToInject); if (onContentInjected != null) { onContentInjected.run(); } - downstream.write(array, off + idx, len - idx); - bytesWritten += (len - idx); + bytesToWrite = len - idx; + downstream.write(array, off + idx, bytesToWrite); + bytesWritten += bytesToWrite; } else { // we don't have a full match. write everything in a bulk except the lookbehind buffer // sequentially @@ -146,8 +148,9 @@ public void write(char[] array, int off, int len) throws IOException { // will be reset if no errors after the following write filter = false; - downstream.write(array, off + marker.length - 1, len - bulkWriteThreshold); - bytesWritten += (len - bulkWriteThreshold); + int bytesToWrite = len - bulkWriteThreshold; + downstream.write(array, off + marker.length - 1, bytesToWrite); + bytesWritten += bytesToWrite; filter = wasFiltering; for (int i = len - marker.length + 1; i < len; i++) { From f7607baa6265b255a153c6ac30288445407e4e90 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 18 Aug 2025 16:33:22 -0400 Subject: [PATCH 26/31] Use dynamic servlet version retrieval --- .../RumHttpServletRequestWrapper.java | 3 +- .../RumHttpServletResponseWrapper.java | 34 ++++++++++++------ .../servlet3/Servlet3Advice.java | 3 +- .../RumHttpServletResponseWrapperTest.groovy | 8 ++++- .../JakartaServletInstrumentation.java | 4 ++- .../RumHttpServletRequestWrapper.java | 3 +- .../RumHttpServletResponseWrapper.java | 35 ++++++++++++------- .../RumHttpServletResponseWrapperTest.groovy | 8 ++++- 8 files changed, 69 insertions(+), 29 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletRequestWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletRequestWrapper.java index e0b4240097e..36be99b0a7f 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletRequestWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletRequestWrapper.java @@ -37,7 +37,8 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se ServletResponse actualResponse = servletResponse; // rewrap it if (servletResponse instanceof HttpServletResponse) { - actualResponse = new RumHttpServletResponseWrapper((HttpServletResponse) servletResponse); + actualResponse = + new RumHttpServletResponseWrapper(this, (HttpServletResponse) servletResponse); servletRequest.setAttribute(DD_RUM_INJECTED, actualResponse); } return super.startAsync(servletRequest, actualResponse); diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index f61fbdb77e7..31a646db59c 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -7,13 +7,16 @@ import java.io.PrintWriter; import java.lang.invoke.MethodHandle; import java.nio.charset.Charset; +import javax.servlet.ServletContext; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private final RumInjector rumInjector; + private final String servletVersion; private WrappedServletOutputStream outputStream; private PrintWriter printWriter; private InjectingPipeWriter wrappedPipeWriter; @@ -22,7 +25,6 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private String contentEncoding = null; private static final MethodHandle SET_CONTENT_LENGTH_LONG = getMh("setContentLengthLong"); - private static final String SERVLET_VERSION = "3"; private static MethodHandle getMh(final String name) { try { @@ -37,9 +39,19 @@ private static void sneakyThrow(Throwable e) throws E { throw (E) e; } - public RumHttpServletResponseWrapper(HttpServletResponse response) { + public RumHttpServletResponseWrapper(HttpServletRequest request, HttpServletResponse response) { super(response); this.rumInjector = RumInjector.get(); + + String version = "3"; + ServletContext servletContext = request.getServletContext(); + if (servletContext != null) { + try { + version = String.valueOf(servletContext.getEffectiveMajorVersion()); + } catch (Exception e) { + } + } + this.servletVersion = version; } @Override @@ -48,7 +60,7 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getOutputStream(); } // start timing injection @@ -68,10 +80,10 @@ public ServletOutputStream getOutputStream() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(SERVLET_VERSION, bytes)); + .onInjectionResponseSize(servletVersion, bytes)); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -84,7 +96,7 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getWriter(); } // start timing injection @@ -100,11 +112,11 @@ public PrintWriter getWriter() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(SERVLET_VERSION, bytes)); + .onInjectionResponseSize(servletVersion, bytes)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -126,7 +138,7 @@ public void addHeader(String name, String value) { private void checkForContentSecurityPolicy(String name) { if (name != null && rumInjector.isEnabled()) { if (name.startsWith("Content-Security-Policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(servletVersion); } } } @@ -177,13 +189,13 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.getTelemetryCollector().onInjectionSucceed(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onInjectionSucceed(servletVersion); // calculate total injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime(SERVLET_VERSION, milliseconds); + RumInjector.getTelemetryCollector().onInjectionTime(servletVersion, milliseconds); injectionStartTime = -1; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java index a06cbda8c59..9a8a55f159d 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Advice.java @@ -50,7 +50,8 @@ public static boolean onEnter( if (maybeRumWrapper instanceof RumHttpServletResponseWrapper) { rumServletWrapper = (RumHttpServletResponseWrapper) maybeRumWrapper; } else { - rumServletWrapper = new RumHttpServletResponseWrapper((HttpServletResponse) response); + rumServletWrapper = + new RumHttpServletResponseWrapper(httpServletRequest, (HttpServletResponse) response); httpServletRequest.setAttribute(DD_RUM_INJECTED, rumServletWrapper); response = rumServletWrapper; request = new RumHttpServletRequestWrapper(httpServletRequest, rumServletWrapper); diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 94d8e07552c..4bade122299 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -8,12 +8,16 @@ import datadog.trace.instrumentation.servlet3.WrappedServletOutputStream import spock.lang.Subject import java.util.function.LongConsumer +import javax.servlet.ServletContext +import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse class RumHttpServletResponseWrapperTest extends AgentTestRunner { private static final String SERVLET_VERSION = "3" + def mockRequest = Mock(HttpServletRequest) def mockResponse = Mock(HttpServletResponse) + def mockServletContext = Mock(ServletContext) def mockTelemetryCollector = Mock(RumTelemetryCollector) // injector needs to be enabled in order to check headers @@ -30,7 +34,9 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { RumHttpServletResponseWrapper wrapper void setup() { - wrapper = new RumHttpServletResponseWrapper(mockResponse) + mockRequest.getServletContext() >> mockServletContext + mockServletContext.getEffectiveMajorVersion() >> 3 + wrapper = new RumHttpServletResponseWrapper(mockRequest, mockResponse) RumInjector.setTelemetryCollector(mockTelemetryCollector) } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/JakartaServletInstrumentation.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/JakartaServletInstrumentation.java index d3630cc1fd8..3d0013786ee 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/JakartaServletInstrumentation.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/JakartaServletInstrumentation.java @@ -84,7 +84,9 @@ public static AgentSpan before( if (maybeRumWrapper instanceof RumHttpServletResponseWrapper) { rumServletWrapper = (RumHttpServletResponseWrapper) maybeRumWrapper; } else { - rumServletWrapper = new RumHttpServletResponseWrapper((HttpServletResponse) response); + rumServletWrapper = + new RumHttpServletResponseWrapper( + httpServletRequest, (HttpServletResponse) response); httpServletRequest.setAttribute(DD_RUM_INJECTED, rumServletWrapper); response = rumServletWrapper; request = new RumHttpServletRequestWrapper(httpServletRequest, rumServletWrapper); diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletRequestWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletRequestWrapper.java index c2a05680488..7bba1a13c10 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletRequestWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletRequestWrapper.java @@ -37,7 +37,8 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se ServletResponse actualResponse = servletResponse; // rewrap it if (servletResponse instanceof HttpServletResponse) { - actualResponse = new RumHttpServletResponseWrapper((HttpServletResponse) servletResponse); + actualResponse = + new RumHttpServletResponseWrapper(this, (HttpServletResponse) servletResponse); servletRequest.setAttribute(DD_RUM_INJECTED, actualResponse); } return super.startAsync(servletRequest, actualResponse); diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 3e65164514e..6768696db22 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -2,7 +2,9 @@ import datadog.trace.api.rum.RumInjector; import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeWriter; +import jakarta.servlet.ServletContext; import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; import java.io.IOException; @@ -11,6 +13,7 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private final RumInjector rumInjector; + private final String servletVersion; private WrappedServletOutputStream outputStream; private InjectingPipeWriter wrappedPipeWriter; private PrintWriter printWriter; @@ -18,11 +21,19 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private long injectionStartTime = -1; private String contentEncoding = null; - private static final String SERVLET_VERSION = "5"; - - public RumHttpServletResponseWrapper(HttpServletResponse response) { + public RumHttpServletResponseWrapper(HttpServletRequest request, HttpServletResponse response) { super(response); this.rumInjector = RumInjector.get(); + + String version = "5"; + ServletContext servletContext = request.getServletContext(); + if (servletContext != null) { + try { + version = String.valueOf(servletContext.getEffectiveMajorVersion()); + } catch (Exception e) { + } + } + this.servletVersion = version; } @Override @@ -31,7 +42,7 @@ public ServletOutputStream getOutputStream() throws IOException { return outputStream; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getOutputStream(); } // start timing injection @@ -51,10 +62,10 @@ public ServletOutputStream getOutputStream() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(SERVLET_VERSION, bytes)); + .onInjectionResponseSize(servletVersion, bytes)); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } return outputStream; @@ -66,7 +77,7 @@ public PrintWriter getWriter() throws IOException { return printWriter; } if (!shouldInject) { - RumInjector.getTelemetryCollector().onInjectionSkipped(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getWriter(); } // start timing injection @@ -82,11 +93,11 @@ public PrintWriter getWriter() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(SERVLET_VERSION, bytes)); + .onInjectionResponseSize(servletVersion, bytes)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { injectionStartTime = -1; - RumInjector.getTelemetryCollector().onInjectionFailed(SERVLET_VERSION, contentEncoding); + RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -108,7 +119,7 @@ public void addHeader(String name, String value) { private void checkForContentSecurityPolicy(String name) { if (name != null && rumInjector.isEnabled()) { if (name.startsWith("Content-Security-Policy")) { - RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(servletVersion); } } } @@ -155,13 +166,13 @@ public void resetBuffer() { } public void onInjected() { - RumInjector.getTelemetryCollector().onInjectionSucceed(SERVLET_VERSION); + RumInjector.getTelemetryCollector().onInjectionSucceed(servletVersion); // calculate total injection time if (injectionStartTime != -1) { long nanoseconds = System.nanoTime() - injectionStartTime; long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime(SERVLET_VERSION, milliseconds); + RumInjector.getTelemetryCollector().onInjectionTime(servletVersion, milliseconds); injectionStartTime = -1; } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index d0a33eb5c11..63fb1c1405f 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -8,12 +8,16 @@ import datadog.trace.instrumentation.servlet5.WrappedServletOutputStream import spock.lang.Subject import java.util.function.LongConsumer +import jakarta.servlet.ServletContext +import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse class RumHttpServletResponseWrapperTest extends AgentTestRunner { private static final String SERVLET_VERSION = "5" + def mockRequest = Mock(HttpServletRequest) def mockResponse = Mock(HttpServletResponse) + def mockServletContext = Mock(ServletContext) def mockTelemetryCollector = Mock(RumTelemetryCollector) // injector needs to be enabled in order to check headers @@ -30,7 +34,9 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { RumHttpServletResponseWrapper wrapper void setup() { - wrapper = new RumHttpServletResponseWrapper(mockResponse) + mockRequest.getServletContext() >> mockServletContext + mockServletContext.getEffectiveMajorVersion() >> 5 + wrapper = new RumHttpServletResponseWrapper(mockRequest, mockResponse) RumInjector.setTelemetryCollector(mockTelemetryCollector) } From e6afc5291061a5a220be61634e05fd5b44b282ac Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 18 Aug 2025 17:39:42 -0400 Subject: [PATCH 27/31] Change injection timing logic --- .../buffer/InjectingPipeOutputStream.java | 19 ++++++- .../buffer/InjectingPipeWriter.java | 19 ++++++- .../InjectingPipeOutputStreamTest.groovy | 52 +++++++++++++++++-- .../buffer/InjectingPipeWriterTest.groovy | 52 +++++++++++++++++-- .../RumHttpServletResponseWrapper.java | 31 +++-------- .../servlet3/WrappedServletOutputStream.java | 5 +- .../RumHttpServletResponseWrapperTest.groovy | 50 +++++++++++++----- .../RumHttpServletResponseWrapper.java | 31 +++-------- .../servlet5/WrappedServletOutputStream.java | 5 +- .../RumHttpServletResponseWrapperTest.groovy | 50 +++++++++++++----- 10 files changed, 230 insertions(+), 84 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index 686170ea380..7221a85d007 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -25,6 +25,7 @@ public class InjectingPipeOutputStream extends OutputStream { private final int bulkWriteThreshold; private final OutputStream downstream; private final LongConsumer onBytesWritten; + private final LongConsumer onInjectionTime; private long bytesWritten = 0; /** @@ -37,7 +38,7 @@ public class InjectingPipeOutputStream extends OutputStream { */ public InjectingPipeOutputStream( final OutputStream downstream, final byte[] marker, final byte[] contentToInject) { - this(downstream, marker, contentToInject, null, null); + this(downstream, marker, contentToInject, null, null, null); } /** @@ -48,13 +49,16 @@ public InjectingPipeOutputStream( * @param contentToInject the content to inject once before the marker if found. * @param onContentInjected callback called when and if the content is injected. * @param onBytesWritten callback called when stream is closed to report total bytes written. + * @param onInjectionTime callback called with the time in milliseconds taken to write the + * injection content. */ public InjectingPipeOutputStream( final OutputStream downstream, final byte[] marker, final byte[] contentToInject, final Runnable onContentInjected, - final LongConsumer onBytesWritten) { + final LongConsumer onBytesWritten, + final LongConsumer onInjectionTime) { this.downstream = downstream; this.marker = marker; this.lookbehind = new byte[marker.length]; @@ -67,6 +71,7 @@ public InjectingPipeOutputStream( this.contentToInject = contentToInject; this.onContentInjected = onContentInjected; this.onBytesWritten = onBytesWritten; + this.onInjectionTime = onInjectionTime; this.bulkWriteThreshold = marker.length * 2 - 2; } @@ -95,7 +100,12 @@ public void write(int b) throws IOException { if (marker[matchingPos++] == b) { if (matchingPos == marker.length) { filter = false; + long injectionStart = System.nanoTime(); downstream.write(contentToInject); + long injectionEnd = System.nanoTime(); + if (onInjectionTime != null) { + onInjectionTime.accept((injectionEnd - injectionStart) / 1_000_000L); + } if (onContentInjected != null) { onContentInjected.run(); } @@ -130,7 +140,12 @@ public void write(byte[] array, int off, int len) throws IOException { int bytesToWrite = idx; downstream.write(array, off, bytesToWrite); bytesWritten += bytesToWrite; + long injectionStart = System.nanoTime(); downstream.write(contentToInject); + long injectionEnd = System.nanoTime(); + if (onInjectionTime != null) { + onInjectionTime.accept((injectionEnd - injectionStart) / 1_000_000L); + } if (onContentInjected != null) { onContentInjected.run(); } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index 83969617a84..8d2c222d924 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -25,6 +25,7 @@ public class InjectingPipeWriter extends Writer { private final int bulkWriteThreshold; private final Writer downstream; private final LongConsumer onBytesWritten; + private final LongConsumer onInjectionTime; private long bytesWritten = 0; /** @@ -37,7 +38,7 @@ public class InjectingPipeWriter extends Writer { */ public InjectingPipeWriter( final Writer downstream, final char[] marker, final char[] contentToInject) { - this(downstream, marker, contentToInject, null, null); + this(downstream, marker, contentToInject, null, null, null); } /** @@ -48,13 +49,16 @@ public InjectingPipeWriter( * @param contentToInject the content to inject once before the marker if found. * @param onContentInjected callback called when and if the content is injected. * @param onBytesWritten callback called when writer is closed to report total bytes written. + * @param onInjectionTime callback called with the time in milliseconds taken to write the + * injection content. */ public InjectingPipeWriter( final Writer downstream, final char[] marker, final char[] contentToInject, final Runnable onContentInjected, - final LongConsumer onBytesWritten) { + final LongConsumer onBytesWritten, + final LongConsumer onInjectionTime) { this.downstream = downstream; this.marker = marker; this.lookbehind = new char[marker.length]; @@ -67,6 +71,7 @@ public InjectingPipeWriter( this.contentToInject = contentToInject; this.onContentInjected = onContentInjected; this.onBytesWritten = onBytesWritten; + this.onInjectionTime = onInjectionTime; this.bulkWriteThreshold = marker.length * 2 - 2; } @@ -95,7 +100,12 @@ public void write(int c) throws IOException { if (marker[matchingPos++] == c) { if (matchingPos == marker.length) { filter = false; + long injectionStart = System.nanoTime(); downstream.write(contentToInject); + long injectionEnd = System.nanoTime(); + if (onInjectionTime != null) { + onInjectionTime.accept((injectionEnd - injectionStart) / 1_000_000L); + } if (onContentInjected != null) { onContentInjected.run(); } @@ -130,7 +140,12 @@ public void write(char[] array, int off, int len) throws IOException { int bytesToWrite = idx; downstream.write(array, off, bytesToWrite); bytesWritten += bytesToWrite; + long injectionStart = System.nanoTime(); downstream.write(contentToInject); + long injectionEnd = System.nanoTime(); + if (onInjectionTime != null) { + onInjectionTime.accept((injectionEnd - injectionStart) / 1_000_000L); + } if (onContentInjected != null) { onContentInjected.run(); } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index 41d275d37c0..a7f6a068d18 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.bootstrap.instrumentation.buffer import datadog.trace.test.util.DDSpecification +import java.util.function.LongConsumer class InjectingPipeOutputStreamTest extends DDSpecification { static final byte[] MARKER_BYTES = "".getBytes("UTF-8") @@ -104,7 +105,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { def testBytes = "test content".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() def counter = new Counter() - def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }, null) when: piped.write(testBytes) @@ -120,7 +121,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { def testBytes = "test".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() def counter = new Counter() - def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }, null) when: for (int i = 0; i < testBytes.length; i++) { @@ -141,7 +142,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { def testBytes = "test content".getBytes("UTF-8") def downstream = new ByteArrayOutputStream() def counter = new Counter() - def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }, null) when: piped.write(part1) @@ -168,4 +169,49 @@ class InjectingPipeOutputStreamTest extends DDSpecification { noExceptionThrown() downstream.toByteArray() == testBytes } + + def 'should call timing callback when injection happens'() { + setup: + def downstream = Mock(OutputStream) { + write(_) >> { args -> + Thread.sleep(1) // simulate slow write + } + } + def timingCallback = Mock(LongConsumer) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, null, timingCallback) + + when: + piped.write("".getBytes("UTF-8")) + piped.close() + + then: + 1 * timingCallback.accept({ it > 0 }) + } + + def 'should not call timing callback when no injection happens'() { + setup: + def downstream = new ByteArrayOutputStream() + def timingCallback = Mock(LongConsumer) + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, null, timingCallback) + + when: + piped.write("no marker here".getBytes("UTF-8")) + piped.close() + + then: + 0 * timingCallback.accept(_) + } + + def 'should be resilient to exceptions when timing callback is null'() { + setup: + def downstream = new ByteArrayOutputStream() + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, null, null) + + when: + piped.write("".getBytes("UTF-8")) + piped.close() + + then: + noExceptionThrown() + } } diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index 049922045f0..f33872a5c5f 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.bootstrap.instrumentation.buffer import datadog.trace.test.util.DDSpecification +import java.util.function.LongConsumer class InjectingPipeWriterTest extends DDSpecification { static final char[] MARKER_CHARS = "".toCharArray() @@ -119,7 +120,7 @@ class InjectingPipeWriterTest extends DDSpecification { setup: def downstream = new StringWriter() def counter = new Counter() - def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }, null) when: piped.write("test content".toCharArray()) @@ -134,7 +135,7 @@ class InjectingPipeWriterTest extends DDSpecification { setup: def downstream = new StringWriter() def counter = new Counter() - def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }, null) when: def content = "test" @@ -152,7 +153,7 @@ class InjectingPipeWriterTest extends DDSpecification { setup: def downstream = new StringWriter() def counter = new Counter() - def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }, null) when: piped.write("test".toCharArray()) @@ -178,4 +179,49 @@ class InjectingPipeWriterTest extends DDSpecification { noExceptionThrown() downstream.toString() == "test content" } + + def 'should call timing callback when injection happens'() { + setup: + def downstream = Mock(Writer) { + write(_) >> { args -> + Thread.sleep(1) // simulate slow write + } + } + def timingCallback = Mock(LongConsumer) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, null, timingCallback) + + when: + piped.write("".toCharArray()) + piped.close() + + then: + 1 * timingCallback.accept({ it > 0 }) + } + + def 'should not call timing callback when no injection happens'() { + setup: + def downstream = new StringWriter() + def timingCallback = Mock(LongConsumer) + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, null, timingCallback) + + when: + piped.write("no marker here".toCharArray()) + piped.close() + + then: + 0 * timingCallback.accept(_) + } + + def 'should be resilient to exceptions when timing callback is null'() { + setup: + def downstream = new StringWriter() + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, null, null) + + when: + piped.write("".toCharArray()) + piped.close() + + then: + noExceptionThrown() + } } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index 31a646db59c..a390b3129af 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -21,7 +21,6 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private PrintWriter printWriter; private InjectingPipeWriter wrappedPipeWriter; private boolean shouldInject = true; - private long injectionStartTime = -1; private String contentEncoding = null; private static final MethodHandle SET_CONTENT_LENGTH_LONG = getMh("setContentLengthLong"); @@ -63,10 +62,6 @@ public ServletOutputStream getOutputStream() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getOutputStream(); } - // start timing injection - if (injectionStartTime == -1) { - injectionStartTime = System.nanoTime(); - } try { String encoding = getCharacterEncoding(); if (encoding == null) { @@ -80,9 +75,11 @@ public ServletOutputStream getOutputStream() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(servletVersion, bytes)); + .onInjectionResponseSize(servletVersion, bytes), + milliseconds -> + RumInjector.getTelemetryCollector() + .onInjectionTime(servletVersion, milliseconds)); } catch (Exception e) { - injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -99,10 +96,6 @@ public PrintWriter getWriter() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getWriter(); } - // start timing injection - if (injectionStartTime == -1) { - injectionStartTime = System.nanoTime(); - } try { wrappedPipeWriter = new InjectingPipeWriter( @@ -112,10 +105,12 @@ public PrintWriter getWriter() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(servletVersion, bytes)); + .onInjectionResponseSize(servletVersion, bytes), + milliseconds -> + RumInjector.getTelemetryCollector() + .onInjectionTime(servletVersion, milliseconds)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { - injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -176,7 +171,6 @@ public void reset() { this.wrappedPipeWriter = null; this.printWriter = null; this.shouldInject = false; - this.injectionStartTime = -1; super.reset(); } @@ -190,15 +184,6 @@ public void resetBuffer() { public void onInjected() { RumInjector.getTelemetryCollector().onInjectionSucceed(servletVersion); - - // calculate total injection time - if (injectionStartTime != -1) { - long nanoseconds = System.nanoTime() - injectionStartTime; - long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime(servletVersion, milliseconds); - injectionStartTime = -1; - } - try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java index a539adadfec..c4b575836ff 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java @@ -34,10 +34,11 @@ public WrappedServletOutputStream( byte[] marker, byte[] contentToInject, Runnable onInjected, - LongConsumer onBytesWritten) { + LongConsumer onBytesWritten, + LongConsumer onInjectionTime) { this.filtered = new InjectingPipeOutputStream( - delegate, marker, contentToInject, onInjected, onBytesWritten); + delegate, marker, contentToInject, onInjected, onBytesWritten, onInjectionTime); this.delegate = delegate; } diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 4bade122299..1472a218483 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -171,7 +171,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } // Callback is created in the RumHttpServletResponseWrapper and passed to InjectingPipeOutputStream via WrappedServletOutputStream. - // When the stream is closed, the callback is called with the total number of bytes written to the stream. + // When the stream is closed, the callback is called with the number of bytes written to the stream and the time taken to write the injection content. void 'response sizes are reported to the telemetry collector via the WrappedServletOutputStream callback'() { setup: def downstream = Mock(javax.servlet.ServletOutputStream) @@ -199,7 +199,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def contentToInject = "".getBytes("UTF-8") def onBytesWritten = Mock(LongConsumer) def stream = new InjectingPipeOutputStream( - downstream, marker, contentToInject, null, onBytesWritten) + downstream, marker, contentToInject, null, onBytesWritten, null) when: stream.write("test".getBytes("UTF-8")) @@ -217,7 +217,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def contentToInject = "".toCharArray() def onBytesWritten = Mock(LongConsumer) def writer = new InjectingPipeWriter( - downstream, marker, contentToInject, null, onBytesWritten) + downstream, marker, contentToInject, null, onBytesWritten, null) when: writer.write("test".toCharArray()) @@ -228,19 +228,45 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * onBytesWritten.accept(11) } - void 'injection timing is reported when injection is successful'() { + void 'injection timing is reported by the InjectingPipeOutputStream callback'() { setup: - wrapper.setContentType("text/html") - def mockWriter = Mock(java.io.PrintWriter) - mockResponse.getWriter() >> mockWriter + def downstream = Mock(java.io.OutputStream) { + write(_) >> { args -> + Thread.sleep(1) // simulate slow write + } + } + def marker = "".getBytes("UTF-8") + def contentToInject = "".getBytes("UTF-8") + def onInjectionTime = Mock(LongConsumer) + def stream = new InjectingPipeOutputStream( + downstream, marker, contentToInject, null, null, onInjectionTime) when: - wrapper.getWriter() - Thread.sleep(1) // ensure measurable time passes - wrapper.onInjected() + stream.write("content".getBytes("UTF-8")) + stream.close() then: - 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) - 1 * mockTelemetryCollector.onInjectionTime(SERVLET_VERSION, { it > 0 }) + 1 * onInjectionTime.accept({ it > 0 }) + } + + void 'injection timing is reported by the InjectingPipeWriter callback'() { + setup: + def downstream = Mock(java.io.Writer) { + write(_) >> { args -> + Thread.sleep(1) // simulate slow write + } + } + def marker = "".toCharArray() + def contentToInject = "".toCharArray() + def onInjectionTime = Mock(LongConsumer) + def writer = new InjectingPipeWriter( + downstream, marker, contentToInject, null, null, onInjectionTime) + + when: + writer.write("content".toCharArray()) + writer.close() + + then: + 1 * onInjectionTime.accept({ it > 0 }) } } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 6768696db22..9a0692ab23d 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -18,7 +18,6 @@ public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { private InjectingPipeWriter wrappedPipeWriter; private PrintWriter printWriter; private boolean shouldInject = true; - private long injectionStartTime = -1; private String contentEncoding = null; public RumHttpServletResponseWrapper(HttpServletRequest request, HttpServletResponse response) { @@ -45,10 +44,6 @@ public ServletOutputStream getOutputStream() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getOutputStream(); } - // start timing injection - if (injectionStartTime == -1) { - injectionStartTime = System.nanoTime(); - } try { String encoding = getCharacterEncoding(); if (encoding == null) { @@ -62,9 +57,11 @@ public ServletOutputStream getOutputStream() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(servletVersion, bytes)); + .onInjectionResponseSize(servletVersion, bytes), + milliseconds -> + RumInjector.getTelemetryCollector() + .onInjectionTime(servletVersion, milliseconds)); } catch (Exception e) { - injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -80,10 +77,6 @@ public PrintWriter getWriter() throws IOException { RumInjector.getTelemetryCollector().onInjectionSkipped(servletVersion); return super.getWriter(); } - // start timing injection - if (injectionStartTime == -1) { - injectionStartTime = System.nanoTime(); - } try { wrappedPipeWriter = new InjectingPipeWriter( @@ -93,10 +86,12 @@ public PrintWriter getWriter() throws IOException { this::onInjected, bytes -> RumInjector.getTelemetryCollector() - .onInjectionResponseSize(servletVersion, bytes)); + .onInjectionResponseSize(servletVersion, bytes), + milliseconds -> + RumInjector.getTelemetryCollector() + .onInjectionTime(servletVersion, milliseconds)); printWriter = new PrintWriter(wrappedPipeWriter); } catch (Exception e) { - injectionStartTime = -1; RumInjector.getTelemetryCollector().onInjectionFailed(servletVersion, contentEncoding); throw e; } @@ -153,7 +148,6 @@ public void reset() { this.wrappedPipeWriter = null; this.printWriter = null; this.shouldInject = false; - this.injectionStartTime = -1; super.reset(); } @@ -167,15 +161,6 @@ public void resetBuffer() { public void onInjected() { RumInjector.getTelemetryCollector().onInjectionSucceed(servletVersion); - - // calculate total injection time - if (injectionStartTime != -1) { - long nanoseconds = System.nanoTime() - injectionStartTime; - long milliseconds = nanoseconds / 1_000_000L; - RumInjector.getTelemetryCollector().onInjectionTime(servletVersion, milliseconds); - injectionStartTime = -1; - } - try { setHeader("x-datadog-rum-injected", "1"); } catch (Throwable ignored) { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java index daf6dcaaafa..fe2a87774ac 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/WrappedServletOutputStream.java @@ -15,10 +15,11 @@ public WrappedServletOutputStream( byte[] marker, byte[] contentToInject, Runnable onInjected, - LongConsumer onBytesWritten) { + LongConsumer onBytesWritten, + LongConsumer onInjectionTime) { this.filtered = new InjectingPipeOutputStream( - delegate, marker, contentToInject, onInjected, onBytesWritten); + delegate, marker, contentToInject, onInjected, onBytesWritten, onInjectionTime); this.delegate = delegate; } diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 63fb1c1405f..18a68c8bd29 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -171,7 +171,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } // Callback is created in the RumHttpServletResponseWrapper and passed to InjectingPipeOutputStream via WrappedServletOutputStream. - // When the stream is closed, the callback is called with the total number of bytes written to the stream. + // When the stream is closed, the callback is called with the number of bytes written to the stream and the time taken to write the injection content. void 'response sizes are reported to the telemetry collector via the WrappedServletOutputStream callback'() { setup: def downstream = Mock(jakarta.servlet.ServletOutputStream) @@ -199,7 +199,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def contentToInject = "".getBytes("UTF-8") def onBytesWritten = Mock(LongConsumer) def stream = new InjectingPipeOutputStream( - downstream, marker, contentToInject, null, onBytesWritten) + downstream, marker, contentToInject, null, onBytesWritten, null) when: stream.write("test".getBytes("UTF-8")) @@ -217,7 +217,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def contentToInject = "".toCharArray() def onBytesWritten = Mock(LongConsumer) def writer = new InjectingPipeWriter( - downstream, marker, contentToInject, null, onBytesWritten) + downstream, marker, contentToInject, null, onBytesWritten, null) when: writer.write("test".toCharArray()) @@ -228,19 +228,45 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { 1 * onBytesWritten.accept(11) } - void 'injection timing is reported when injection is successful'() { + void 'injection timing is reported by the InjectingPipeOutputStream callback'() { setup: - wrapper.setContentType("text/html") - def mockWriter = Mock(java.io.PrintWriter) - mockResponse.getWriter() >> mockWriter + def downstream = Mock(java.io.OutputStream) { + write(_) >> { args -> + Thread.sleep(1) // simulate slow write + } + } + def marker = "".getBytes("UTF-8") + def contentToInject = "".getBytes("UTF-8") + def onInjectionTime = Mock(LongConsumer) + def stream = new InjectingPipeOutputStream( + downstream, marker, contentToInject, null, null, onInjectionTime) when: - wrapper.getWriter() - Thread.sleep(1) // ensure measurable time passes - wrapper.onInjected() + stream.write("content".getBytes("UTF-8")) + stream.close() then: - 1 * mockTelemetryCollector.onInjectionSucceed(SERVLET_VERSION) - 1 * mockTelemetryCollector.onInjectionTime(SERVLET_VERSION, { it > 0 }) + 1 * onInjectionTime.accept({ it > 0 }) + } + + void 'injection timing is reported by the InjectingPipeWriter callback'() { + setup: + def downstream = Mock(java.io.Writer) { + write(_) >> { args -> + Thread.sleep(1) // simulate slow write + } + } + def marker = "".toCharArray() + def contentToInject = "".toCharArray() + def onInjectionTime = Mock(LongConsumer) + def writer = new InjectingPipeWriter( + downstream, marker, contentToInject, null, null, onInjectionTime) + + when: + writer.write("content".toCharArray()) + writer.close() + + then: + 1 * onInjectionTime.accept({ it > 0 }) } } From 6f2dc97be154387e3b21b4b19ebb40f873e1a264 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Mon, 18 Aug 2025 21:32:02 -0400 Subject: [PATCH 28/31] Clean up --- .../servlet3/RumHttpServletResponseWrapper.java | 4 ++-- .../groovy/RumHttpServletResponseWrapperTest.groovy | 12 +----------- .../servlet5/RumHttpServletResponseWrapper.java | 4 ++-- .../groovy/RumHttpServletResponseWrapperTest.groovy | 12 +----------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java index a390b3129af..a1dc5bcf54d 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -131,7 +131,7 @@ public void addHeader(String name, String value) { } private void checkForContentSecurityPolicy(String name) { - if (name != null && rumInjector.isEnabled()) { + if (name != null) { if (name.startsWith("Content-Security-Policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(servletVersion); } @@ -159,7 +159,7 @@ public void setContentLengthLong(long len) { @Override public void setCharacterEncoding(String charset) { - if (charset != null && rumInjector.isEnabled()) { + if (charset != null) { this.contentEncoding = charset; } super.setCharacterEncoding(charset); diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 1472a218483..ea032c58d7c 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -20,16 +20,6 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def mockServletContext = Mock(ServletContext) def mockTelemetryCollector = Mock(RumTelemetryCollector) - // injector needs to be enabled in order to check headers - @Override - protected void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("rum.enabled", "true") - injectSysConfig("rum.application.id", "test") - injectSysConfig("rum.client.token", "secret") - injectSysConfig("rum.remote.configuration.id", "12345") - } - @Subject RumHttpServletResponseWrapper wrapper @@ -181,7 +171,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, bytes) } def wrappedStream = new WrappedServletOutputStream( - downstream, marker, contentToInject, null, onBytesWritten) + downstream, marker, contentToInject, null, onBytesWritten, null) when: wrappedStream.write("test".getBytes("UTF-8")) diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java index 9a0692ab23d..b242ba07837 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java +++ b/dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java @@ -112,7 +112,7 @@ public void addHeader(String name, String value) { } private void checkForContentSecurityPolicy(String name) { - if (name != null && rumInjector.isEnabled()) { + if (name != null) { if (name.startsWith("Content-Security-Policy")) { RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected(servletVersion); } @@ -136,7 +136,7 @@ public void setContentLengthLong(long len) { @Override public void setCharacterEncoding(String charset) { - if (charset != null && rumInjector.isEnabled()) { + if (charset != null) { this.contentEncoding = charset; } super.setCharacterEncoding(charset); diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index 18a68c8bd29..b543199ee27 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -20,16 +20,6 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def mockServletContext = Mock(ServletContext) def mockTelemetryCollector = Mock(RumTelemetryCollector) - // injector needs to be enabled in order to check headers - @Override - protected void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("rum.enabled", "true") - injectSysConfig("rum.application.id", "test") - injectSysConfig("rum.client.token", "secret") - injectSysConfig("rum.remote.configuration.id", "12345") - } - @Subject RumHttpServletResponseWrapper wrapper @@ -181,7 +171,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, bytes) } def wrappedStream = new WrappedServletOutputStream( - downstream, marker, contentToInject, null, onBytesWritten) + downstream, marker, contentToInject, null, onBytesWritten, null) when: wrappedStream.write("test".getBytes("UTF-8")) From 32372c13b0ff176111931acf3a30b2ab9cd87e22 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Tue, 19 Aug 2025 10:39:26 -0400 Subject: [PATCH 29/31] Use dynamic servlet version retrieval for tagging as well --- .../trace/api/rum/RumInjectorMetrics.java | 169 ++++++++++-------- .../api/rum/RumInjectorMetricsTest.groovy | 38 +++- 2 files changed, 126 insertions(+), 81 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java index 7b67dff6bfd..f97742af3d2 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java @@ -2,6 +2,8 @@ import datadog.trace.api.Config; import datadog.trace.api.StatsDClient; +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; import java.util.concurrent.atomic.AtomicLong; /** @@ -13,40 +15,6 @@ * metrics and tags */ public class RumInjectorMetrics implements RumTelemetryCollector { - // Use static tags for common combinations so that we don't have to build them for each metric - // Note that injector_version tags are not included because we do not use the rust injector - private static final String[] CSP_SERVLET3_TAGS = - new String[] { - "integration_name:servlet", - "integration_version:3", - "kind:header", - "reason:csp_header_found", - "status:seen" - }; - - private static final String[] CSP_SERVLET5_TAGS = - new String[] { - "integration_name:servlet", - "integration_version:5", - "kind:header", - "reason:csp_header_found", - "status:seen" - }; - - private static final String[] INIT_TAGS = - new String[] {"integration_name:servlet", "integration_version:3,5"}; - - private static final String[] TIME_SERVLET3_TAGS = - new String[] {"integration_name:servlet", "integration_version:3"}; - - private static final String[] TIME_SERVLET5_TAGS = - new String[] {"integration_name:servlet", "integration_version:5"}; - - private static final String[] RESPONSE_SERVLET3_TAGS = - new String[] {"integration_name:servlet", "integration_version:3", "response_kind:header"}; - - private static final String[] RESPONSE_SERVLET5_TAGS = - new String[] {"integration_name:servlet", "integration_version:5", "response_kind:header"}; private final AtomicLong injectionSucceed = new AtomicLong(); private final AtomicLong injectionFailed = new AtomicLong(); @@ -59,6 +27,17 @@ public class RumInjectorMetrics implements RumTelemetryCollector { private final String applicationId; private final String remoteConfigUsed; + // Cache dependent on servlet version and content encoding + private final DDCache succeedTagsCache = DDCaches.newFixedSizeCache(8); + private final DDCache skippedTagsCache = DDCaches.newFixedSizeCache(8); + private final DDCache cspTagsCache = DDCaches.newFixedSizeCache(8); + private final DDCache responseTagsCache = DDCaches.newFixedSizeCache(8); + private final DDCache timeTagsCache = DDCaches.newFixedSizeCache(8); + private final DDCache failedTagsCache = DDCaches.newFixedSizeCache(16); + + private static final String[] INIT_TAGS = + new String[] {"integration_name:servlet", "integration_version:N/A"}; + public RumInjectorMetrics(final StatsDClient statsd) { this.statsd = statsd; @@ -75,61 +54,70 @@ public RumInjectorMetrics(final StatsDClient statsd) { } @Override - public void onInjectionSucceed(String integrationVersion) { + public void onInjectionSucceed(String servletVersion) { injectionSucceed.incrementAndGet(); String[] tags = - new String[] { - "application_id:" + applicationId, - "integration_name:servlet", - "integration_version:" + integrationVersion, - "remote_config_used:" + remoteConfigUsed - }; + succeedTagsCache.computeIfAbsent( + servletVersion, + version -> + new String[] { + "application_id:" + applicationId, + "integration_name:servlet", + "integration_version:" + version, + "remote_config_used:" + remoteConfigUsed + }); statsd.count("rum.injection.succeed", 1, tags); } @Override - public void onInjectionFailed(String integrationVersion, String contentEncoding) { + public void onInjectionFailed(String servletVersion, String contentEncoding) { injectionFailed.incrementAndGet(); - String[] tags; - if (contentEncoding != null) { - tags = - new String[] { - "application_id:" + applicationId, - "content_encoding:" + contentEncoding, - "integration_name:servlet", - "integration_version:" + integrationVersion, - "reason:failed_to_return_response_wrapper", - "remote_config_used:" + remoteConfigUsed - }; - } else { - tags = - new String[] { - "application_id:" + applicationId, - "integration_name:servlet", - "integration_version:" + integrationVersion, - "reason:failed_to_return_response_wrapper", - "remote_config_used:" + remoteConfigUsed - }; - } + String cacheKey = servletVersion + ":" + contentEncoding; + String[] tags = + failedTagsCache.computeIfAbsent( + cacheKey, + key -> { + if (contentEncoding != null) { + return new String[] { + "application_id:" + applicationId, + "content_encoding:" + contentEncoding, + "integration_name:servlet", + "integration_version:" + servletVersion, + "reason:failed_to_return_response_wrapper", + "remote_config_used:" + remoteConfigUsed + }; + } else { + return new String[] { + "application_id:" + applicationId, + "integration_name:servlet", + "integration_version:" + servletVersion, + "reason:failed_to_return_response_wrapper", + "remote_config_used:" + remoteConfigUsed + }; + } + }); statsd.count("rum.injection.failed", 1, tags); } @Override - public void onInjectionSkipped(String integrationVersion) { + public void onInjectionSkipped(String servletVersion) { injectionSkipped.incrementAndGet(); String[] tags = - new String[] { - "application_id:" + applicationId, - "integration_name:servlet", - "integration_version:" + integrationVersion, - "reason:should_not_inject", - "remote_config_used:" + remoteConfigUsed - }; + skippedTagsCache.computeIfAbsent( + servletVersion, + version -> + new String[] { + "application_id:" + applicationId, + "integration_name:servlet", + "integration_version:" + version, + "reason:should_not_inject", + "remote_config_used:" + remoteConfigUsed + }); statsd.count("rum.injection.skipped", 1, tags); } @@ -141,23 +129,43 @@ public void onInitializationSucceed() { } @Override - public void onContentSecurityPolicyDetected(String integrationVersion) { + public void onContentSecurityPolicyDetected(String servletVersion) { contentSecurityPolicyDetected.incrementAndGet(); - String[] tags = "5".equals(integrationVersion) ? CSP_SERVLET5_TAGS : CSP_SERVLET3_TAGS; + String[] tags = + cspTagsCache.computeIfAbsent( + servletVersion, + version -> + new String[] { + "integration_name:servlet", + "integration_version:" + version, + "kind:header", + "reason:csp_header_found", + "status:seen" + }); statsd.count("rum.injection.content_security_policy", 1, tags); } @Override - public void onInjectionResponseSize(String integrationVersion, long bytes) { + public void onInjectionResponseSize(String servletVersion, long bytes) { String[] tags = - "5".equals(integrationVersion) ? RESPONSE_SERVLET5_TAGS : RESPONSE_SERVLET3_TAGS; + responseTagsCache.computeIfAbsent( + servletVersion, + version -> + new String[] { + "integration_name:servlet", + "integration_version:" + version, + "response_kind:header" + }); statsd.distribution("rum.injection.response.bytes", bytes, tags); } @Override - public void onInjectionTime(String integrationVersion, long milliseconds) { - String[] tags = "5".equals(integrationVersion) ? TIME_SERVLET5_TAGS : TIME_SERVLET3_TAGS; + public void onInjectionTime(String servletVersion, long milliseconds) { + String[] tags = + timeTagsCache.computeIfAbsent( + servletVersion, + version -> new String[] {"integration_name:servlet", "integration_version:" + version}); statsd.distribution("rum.injection.ms", milliseconds, tags); } @@ -168,6 +176,13 @@ public void close() { injectionSkipped.set(0); contentSecurityPolicyDetected.set(0); initializationSucceed.set(0); + + succeedTagsCache.clear(); + skippedTagsCache.clear(); + cspTagsCache.clear(); + responseTagsCache.clear(); + timeTagsCache.clear(); + failedTagsCache.clear(); } public String summary() { diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 286cfcfb4db..3cbfab294d1 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -22,6 +22,7 @@ class RumInjectorMetricsTest extends Specification { when: metrics.onInjectionSucceed("3") metrics.onInjectionSucceed("5") + metrics.onInjectionSucceed("6") then: 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> @@ -32,6 +33,10 @@ class RumInjectorMetricsTest extends Specification { def tags = args[2] as String[] assertTags(tags, "integration_name:servlet", "integration_version:5") } + 1 * statsD.count('rum.injection.succeed', 1, _) >> { args -> + def tags = args[2] as String[] + assertTags(tags, "integration_name:servlet", "integration_version:6") + } 0 * _ } @@ -39,6 +44,7 @@ class RumInjectorMetricsTest extends Specification { when: metrics.onInjectionFailed("3", "gzip") metrics.onInjectionFailed("5", null) + metrics.onInjectionFailed("6", "gzip") then: 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> @@ -50,6 +56,10 @@ class RumInjectorMetricsTest extends Specification { assert !tags.any { it.startsWith("content_encoding:") } assertTags(tags, "integration_name:servlet", "integration_version:5", "reason:failed_to_return_response_wrapper") } + 1 * statsD.count('rum.injection.failed', 1, _) >> { args -> + def tags = args[2] as String[] + assertTags(tags, "content_encoding:gzip", "integration_name:servlet", "integration_version:6", "reason:failed_to_return_response_wrapper") + } 0 * _ } @@ -57,6 +67,7 @@ class RumInjectorMetricsTest extends Specification { when: metrics.onInjectionSkipped("3") metrics.onInjectionSkipped("5") + metrics.onInjectionSkipped("6") then: 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> @@ -67,6 +78,10 @@ class RumInjectorMetricsTest extends Specification { def tags = args[2] as String[] assertTags(tags, "integration_name:servlet", "integration_version:5", "reason:should_not_inject") } + 1 * statsD.count('rum.injection.skipped', 1, _) >> { args -> + def tags = args[2] as String[] + assertTags(tags, "integration_name:servlet", "integration_version:6", "reason:should_not_inject") + } 0 * _ } @@ -74,6 +89,7 @@ class RumInjectorMetricsTest extends Specification { when: metrics.onContentSecurityPolicyDetected("3") metrics.onContentSecurityPolicyDetected("5") + metrics.onContentSecurityPolicyDetected("6") then: 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> @@ -84,6 +100,10 @@ class RumInjectorMetricsTest extends Specification { def tags = args[2] as String[] assertTags(tags, "integration_name:servlet", "integration_version:5", "kind:header", "reason:csp_header_found", "status:seen") } + 1 * statsD.count('rum.injection.content_security_policy', 1, _) >> { args -> + def tags = args[2] as String[] + assertTags(tags, "integration_name:servlet", "integration_version:6", "kind:header", "reason:csp_header_found", "status:seen") + } 0 * _ } @@ -94,7 +114,7 @@ class RumInjectorMetricsTest extends Specification { then: 1 * statsD.count('rum.injection.initialization.succeed', 1, _) >> { args -> def tags = args[2] as String[] - assertTags(tags, "integration_name:servlet", "integration_version:3,5") + assertTags(tags, "integration_name:servlet", "integration_version:N/A") } 0 * _ } @@ -103,6 +123,7 @@ class RumInjectorMetricsTest extends Specification { when: metrics.onInjectionResponseSize("3", 256) metrics.onInjectionResponseSize("5", 512) + metrics.onInjectionResponseSize("6", 1024) then: 1 * statsD.distribution('rum.injection.response.bytes', 256, _) >> { args -> @@ -113,6 +134,10 @@ class RumInjectorMetricsTest extends Specification { def tags = args[2] as String[] assertTags(tags, "integration_name:servlet", "integration_version:5", "response_kind:header") } + 1 * statsD.distribution('rum.injection.response.bytes', 1024, _) >> { args -> + def tags = args[2] as String[] + assertTags(tags, "integration_name:servlet", "integration_version:6", "response_kind:header") + } 0 * _ } @@ -120,6 +145,7 @@ class RumInjectorMetricsTest extends Specification { when: metrics.onInjectionTime("5", 5L) metrics.onInjectionTime("3", 10L) + metrics.onInjectionTime("6", 15L) then: 1 * statsD.distribution('rum.injection.ms', 5L, _) >> { args -> @@ -130,6 +156,10 @@ class RumInjectorMetricsTest extends Specification { def tags = args[2] as String[] assertTags(tags, "integration_name:servlet", "integration_version:3") } + 1 * statsD.distribution('rum.injection.ms', 15L, _) >> { args -> + def tags = args[2] as String[] + assertTags(tags, "integration_name:servlet", "integration_version:6") + } 0 * _ } @@ -140,10 +170,10 @@ class RumInjectorMetricsTest extends Specification { metrics.onInjectionSkipped("5") metrics.onInjectionFailed("3", "gzip") metrics.onInjectionSucceed("3") - metrics.onInjectionFailed("5", null) - metrics.onInjectionSucceed("3") + metrics.onInjectionFailed("6", null) + metrics.onInjectionSucceed("6") metrics.onInjectionSkipped("3") - metrics.onContentSecurityPolicyDetected("5") + metrics.onContentSecurityPolicyDetected("6") metrics.onInjectionResponseSize("3", 256) metrics.onInjectionTime("5", 5L) def summary = metrics.summary() From 150ab051fd509f7c70c0484ac43d1e762e10a065 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Tue, 19 Aug 2025 11:35:19 -0400 Subject: [PATCH 30/31] Add a telemetry check to HttpServerTest --- .../trace/agent/test/base/HttpServerTest.groovy | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy index fba553fe199..8bd05e8147a 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -2236,14 +2236,28 @@ abstract class HttpServerTest extends WithHttpServer { def "test rum injection in head for mime #mime"() { setup: assumeTrue(testRumInjection()) + def telemetryCollector = RumInjector.getTelemetryCollector() def request = new Request.Builder().url(server.address().resolve("gimme-$mime").toURL()) .get().build() + when: def response = client.newCall(request).execute() + def responseBody = response.body().string() + def finalSummary = telemetryCollector.summary() + then: assert response.code() == 200 - assert response.body().string().contains(new String(RumInjector.get().getSnippetBytes("UTF-8"), "UTF-8")) == expected + assert responseBody.contains(new String(RumInjector.get().getSnippetBytes("UTF-8"), "UTF-8")) == expected assert response.header("x-datadog-rum-injected") == (expected ? "1" : null) + + // Check a few telemetry metrics + if (expected) { + assert finalSummary.contains("injectionSucceed=") + assert responseBody.length() > 0 + } else { + assert finalSummary.contains("injectionSkipped=") + } + where: mime | expected "html" | true From e9095b558b730fe5659f856a1e1a31363853f32e Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Tue, 19 Aug 2025 15:25:10 -0400 Subject: [PATCH 31/31] Clean up --- .../InjectingPipeOutputStreamTest.groovy | 19 +++++------ .../buffer/InjectingPipeWriterTest.groovy | 33 ++++++++++--------- .../RumHttpServletResponseWrapperTest.groovy | 23 +++++++------ .../RumHttpServletResponseWrapperTest.groovy | 23 +++++++------ .../api/rum/RumInjectorMetricsTest.groovy | 2 +- .../trace/api/rum/RumInjectorTest.groovy | 3 -- 6 files changed, 53 insertions(+), 50 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index a7f6a068d18..fcf4699075d 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -112,7 +112,7 @@ class InjectingPipeOutputStreamTest extends DDSpecification { piped.close() then: - counter.value == 12 + counter.value == testBytes.length downstream.toByteArray() == testBytes } @@ -130,29 +130,26 @@ class InjectingPipeOutputStreamTest extends DDSpecification { piped.close() then: - counter.value == 4 + counter.value == testBytes.length downstream.toByteArray() == testBytes } def 'should count bytes correctly with multiple writes'() { setup: - def part1 = "test".getBytes("UTF-8") - def part2 = " ".getBytes("UTF-8") - def part3 = "content".getBytes("UTF-8") - def testBytes = "test content".getBytes("UTF-8") + def testBytes = "test content" def downstream = new ByteArrayOutputStream() def counter = new Counter() def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, null, { long bytes -> counter.incr(bytes) }, null) when: - piped.write(part1) - piped.write(part2) - piped.write(part3) + piped.write(testBytes[0..4].getBytes("UTF-8")) + piped.write(testBytes[5..5].getBytes("UTF-8")) + piped.write(testBytes[6..-1].getBytes("UTF-8")) piped.close() then: - counter.value == 12 - downstream.toByteArray() == testBytes + counter.value == testBytes.length() + downstream.toByteArray() == testBytes.getBytes("UTF-8") } def 'should be resilient to exceptions when onBytesWritten callback is null'() { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index f33872a5c5f..7466839f7c8 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -121,14 +121,15 @@ class InjectingPipeWriterTest extends DDSpecification { def downstream = new StringWriter() def counter = new Counter() def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }, null) + def testBytes = "test content" when: - piped.write("test content".toCharArray()) + piped.write(testBytes.toCharArray()) piped.close() then: - counter.value == 12 - downstream.toString() == "test content" + counter.value == testBytes.length() + downstream.toString() == testBytes } def 'should count bytes correctly when writing characters individually'() { @@ -136,17 +137,17 @@ class InjectingPipeWriterTest extends DDSpecification { def downstream = new StringWriter() def counter = new Counter() def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }, null) + def testBytes = "test" when: - def content = "test" - for (int i = 0; i < content.length(); i++) { - piped.write((int) content.charAt(i)) + for (int i = 0; i < testBytes.length(); i++) { + piped.write((int) testBytes.charAt(i)) } piped.close() then: - counter.value == 4 - downstream.toString() == "test" + counter.value == testBytes.length() + downstream.toString() == testBytes } def 'should count bytes correctly with multiple writes'() { @@ -154,30 +155,32 @@ class InjectingPipeWriterTest extends DDSpecification { def downstream = new StringWriter() def counter = new Counter() def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, null, { long bytes -> counter.incr(bytes) }, null) + def testBytes = "test content" when: - piped.write("test".toCharArray()) - piped.write(" ".toCharArray()) - piped.write("content".toCharArray()) + piped.write(testBytes[0..4].toCharArray()) + piped.write(testBytes[5..5].toCharArray()) + piped.write(testBytes[6..-1].toCharArray()) piped.close() then: - counter.value == 12 - downstream.toString() == "test content" + counter.value == testBytes.length() + downstream.toString() == testBytes } def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def downstream = new StringWriter() def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS) + def testBytes = "test content" when: - piped.write("test content".toCharArray()) + piped.write(testBytes.toCharArray()) piped.close() then: noExceptionThrown() - downstream.toString() == "test content" + downstream.toString() == testBytes } def 'should call timing callback when injection happens'() { diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index ea032c58d7c..f34fa560ed6 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -25,7 +25,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { void setup() { mockRequest.getServletContext() >> mockServletContext - mockServletContext.getEffectiveMajorVersion() >> 3 + mockServletContext.getEffectiveMajorVersion() >> Integer.parseInt(SERVLET_VERSION) wrapper = new RumHttpServletResponseWrapper(mockRequest, mockResponse) RumInjector.setTelemetryCollector(mockTelemetryCollector) } @@ -172,14 +172,15 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } def wrappedStream = new WrappedServletOutputStream( downstream, marker, contentToInject, null, onBytesWritten, null) + def testBytes = "test content" when: - wrappedStream.write("test".getBytes("UTF-8")) - wrappedStream.write("content".getBytes("UTF-8")) + wrappedStream.write(testBytes[0..5].getBytes("UTF-8")) + wrappedStream.write(testBytes[6..-1].getBytes("UTF-8")) wrappedStream.close() then: - 1 * mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, 11) + 1 * mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, testBytes.length()) } void 'response sizes are reported by the InjectingPipeOutputStream callback'() { @@ -190,14 +191,15 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def onBytesWritten = Mock(LongConsumer) def stream = new InjectingPipeOutputStream( downstream, marker, contentToInject, null, onBytesWritten, null) + def testBytes = "test content" when: - stream.write("test".getBytes("UTF-8")) - stream.write("content".getBytes("UTF-8")) + stream.write(testBytes[0..5].getBytes("UTF-8")) + stream.write(testBytes[6..-1].getBytes("UTF-8")) stream.close() then: - 1 * onBytesWritten.accept(11) + 1 * onBytesWritten.accept(testBytes.length()) } void 'response sizes are reported by the InjectingPipeWriter callback'() { @@ -208,14 +210,15 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def onBytesWritten = Mock(LongConsumer) def writer = new InjectingPipeWriter( downstream, marker, contentToInject, null, onBytesWritten, null) + def testBytes = "test content" when: - writer.write("test".toCharArray()) - writer.write("content".toCharArray()) + writer.write(testBytes[0..5].toCharArray()) + writer.write(testBytes[6..-1].toCharArray()) writer.close() then: - 1 * onBytesWritten.accept(11) + 1 * onBytesWritten.accept(testBytes.length()) } void 'injection timing is reported by the InjectingPipeOutputStream callback'() { diff --git a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy index b543199ee27..78dbd697c0d 100644 --- a/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy +++ b/dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy @@ -25,7 +25,7 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { void setup() { mockRequest.getServletContext() >> mockServletContext - mockServletContext.getEffectiveMajorVersion() >> 5 + mockServletContext.getEffectiveMajorVersion() >> Integer.parseInt(SERVLET_VERSION) wrapper = new RumHttpServletResponseWrapper(mockRequest, mockResponse) RumInjector.setTelemetryCollector(mockTelemetryCollector) } @@ -172,14 +172,15 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { } def wrappedStream = new WrappedServletOutputStream( downstream, marker, contentToInject, null, onBytesWritten, null) + def testBytes = "test content" when: - wrappedStream.write("test".getBytes("UTF-8")) - wrappedStream.write("content".getBytes("UTF-8")) + wrappedStream.write(testBytes[0..5].getBytes("UTF-8")) + wrappedStream.write(testBytes[6..-1].getBytes("UTF-8")) wrappedStream.close() then: - 1 * mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, 11) + 1 * mockTelemetryCollector.onInjectionResponseSize(SERVLET_VERSION, testBytes.length()) } void 'response sizes are reported by the InjectingPipeOutputStream callback'() { @@ -190,14 +191,15 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def onBytesWritten = Mock(LongConsumer) def stream = new InjectingPipeOutputStream( downstream, marker, contentToInject, null, onBytesWritten, null) + def testBytes = "test content" when: - stream.write("test".getBytes("UTF-8")) - stream.write("content".getBytes("UTF-8")) + stream.write(testBytes[0..5].getBytes("UTF-8")) + stream.write(testBytes[6..-1].getBytes("UTF-8")) stream.close() then: - 1 * onBytesWritten.accept(11) + 1 * onBytesWritten.accept(testBytes.length()) } void 'response sizes are reported by the InjectingPipeWriter callback'() { @@ -208,14 +210,15 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner { def onBytesWritten = Mock(LongConsumer) def writer = new InjectingPipeWriter( downstream, marker, contentToInject, null, onBytesWritten, null) + def testBytes = "test content" when: - writer.write("test".toCharArray()) - writer.write("content".toCharArray()) + writer.write(testBytes[0..5].toCharArray()) + writer.write(testBytes[6..-1].toCharArray()) writer.close() then: - 1 * onBytesWritten.accept(11) + 1 * onBytesWritten.accept(testBytes.length()) } void 'injection timing is reported by the InjectingPipeOutputStream callback'() { diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy index 3cbfab294d1..eb7ba338bc0 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy @@ -16,7 +16,7 @@ class RumInjectorMetricsTest extends Specification { } } - // Note: application_id and remote_config_used are dynamic runtime values that depend on + // Note: application_id and remote_config_used tags need dynamic runtime values that depend on // the RUM configuration state, so we do not test them here. def "test onInjectionSucceed"() { when: diff --git a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy index e16939adf8b..1b2fd4783fb 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorTest.groovy @@ -134,10 +134,8 @@ class RumInjectorTest extends DDSpecification { void 'telemetry integration works end-to-end'() { when: - // simulate CoreTracer enabling telemetry RumInjector.enableTelemetry(mock(datadog.trace.api.StatsDClient)) - // simulate reporting injection telemetry def telemetryCollector = RumInjector.getTelemetryCollector() telemetryCollector.onInjectionSucceed("3") telemetryCollector.onInjectionFailed("3", "gzip") @@ -146,7 +144,6 @@ class RumInjectorTest extends DDSpecification { telemetryCollector.onInjectionResponseSize("3", 256) telemetryCollector.onInjectionTime("3", 5L) - // verify metrics are collected def summary = telemetryCollector.summary() then: