From 257f167f913dfb364da5b847c0055892bb0c981d Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 3 Jul 2025 17:45:41 +0200 Subject: [PATCH 01/13] feat(rum): Add initial config and API --- internal-api/build.gradle | 1 + .../main/java/datadog/trace/api/Config.java | 4 + .../trace/api/rum/ContentTypesCache.java | 45 ++++ .../java/datadog/trace/api/rum/RumConfig.java | 198 ++++++++++++++++++ .../datadog/trace/api/rum/RumInjector.java | 18 ++ 5 files changed, 266 insertions(+) create mode 100644 internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java create mode 100644 internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java create mode 100644 internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java diff --git a/internal-api/build.gradle b/internal-api/build.gradle index fbf1916a4af..424873f75be 100644 --- a/internal-api/build.gradle +++ b/internal-api/build.gradle @@ -244,6 +244,7 @@ dependencies { api project(':dd-trace-api') api libs.slf4j api project(':components:context') + api project(':components:json') api project(':components:yaml') api project(':components:cli') api project(":utils:time-utils") diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 6534d8f870c..3140a95d547 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -1190,6 +1190,10 @@ public static String getHostName() { private final int stackTraceLengthLimit; + // RUM config -- start + // TODO + // RUM config -- end + // Read order: System Properties -> Env Variables, [-> properties file], [-> default value] private Config() { this(ConfigProvider.createDefault()); diff --git a/internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java b/internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java new file mode 100644 index 00000000000..59c439c7173 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java @@ -0,0 +1,45 @@ +package datadog.trace.api.rum; + +import java.util.HashMap; +import java.util.Map; + +/* + * Cache for ContentTypes answers + */ +public class ContentTypesCache { + static final short MAX_ITEMS = 1_000; + private final Map map; + + /* + * Constructs a new ContentTypes cache for the provided types + * + * @param contentTypes the list of ContentTypes it will store answers for + */ + public ContentTypesCache(String[] contentTypes) { + this.map = new HashMap<>(); + for (String contentType : contentTypes) { + this.map.put(contentType, true); + } + } + + public boolean contains(String headerValue) { + return this.map.computeIfAbsent(headerValue, this::shouldProcess); + } + + private boolean shouldProcess(String headerValue) { + Boolean typeAllowed = this.map.get(getContentType(headerValue)); + return typeAllowed != null && typeAllowed; + } + + private String getContentType(String headerValue) { + // multipart/* are expected to contain boundary unique values + // let's abort early to avoid exploding the cache + // additionally, if the cache is already too big, let's also abort + if (this.map.size() > MAX_ITEMS || headerValue.startsWith("multipart/")) { + return null; + } + // RFC 2045 defines optional parameters always behind a semicolon + int semicolon = headerValue.indexOf(";"); + return semicolon != -1 ? headerValue.substring(0, semicolon) : headerValue; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java b/internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java new file mode 100644 index 00000000000..f9e241f7bca --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java @@ -0,0 +1,198 @@ +package datadog.trace.api.rum; + +import static java.util.Locale.ROOT; + +import datadog.json.JsonWriter; +import datadog.trace.api.Config; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +public class RumConfig { + private static final String DEFAULT_SITE = "datadoghq.com"; + private static final String GOV_CLOUD_SITE = "ddog-gov.com"; + private static final Map REGIONS = new HashMap<>(); + + static { + REGIONS.put("datadoghq.com", "us1"); + REGIONS.put("us3.datadoghq.com", "us3"); + REGIONS.put("us5.datadoghq.com", "us5"); + REGIONS.put("datadoghq.eu", "eu1"); + REGIONS.put("ap1.datadoghq.com", "ap1"); + } + + /** RUM application ID */ + public final String applicationId; + /** The client token provided by Datadog to authenticate requests. */ + public final String clientToken; + /** The Datadog site to which data will be sent (e.g., `datadoghq.com`). */ + public final String site; + /** The name of the service being monitored. */ + @Nullable public final String service; + /** The environment of the service (e.g., `prod`, `staging` or `dev). */ + @Nullable public final String env; + /** SDK major version. */ + public int majorVersion; + /** The version of the service (e.g., `0.1.0`, `a8dj92`, `2024-30`). */ + @Nullable public final String version; + /** Enables or disables the automatic collection of users actions (e.g., clicks). */ + @Nullable public final Boolean trackUserInteractions; + /** Enables or disables the collection of resource events (e.g., loading of images or scripts). */ + @Nullable public final Boolean trackResources; + /** Enables or disables the collection of long task events. */ + @Nullable public final Boolean trackLongTask; + /** The privacy level for data collection. */ + @Nullable public final PrivacyLevel defaultPrivacyLevel; + /** The percentage of user sessions to be tracked (between 0.0 and 100.0). */ + @Nullable public final Float sessionSampleRate; + /** + * The percentage of tracked sessions that will include Session Replay data (between 0.0 and + * 100.0). + */ + @Nullable public final Float sessionReplaySampleRate; + + public static RumConfig from(Config config) { + // TODO + return null; + } + + RumConfig( + String applicationId, + String clientToken, + @Nullable String site, + @Nullable String service, + @Nullable String env, + int majorVersion, + @Nullable String version, + @Nullable Boolean trackUserInteractions, + @Nullable Boolean trackResources, + @Nullable Boolean trackLongTask, + @Nullable PrivacyLevel defaultPrivacyLevel, + @Nullable Float sessionSampleRate, + @Nullable Float sessionReplaySampleRate) { + if (applicationId == null || applicationId.isEmpty()) { + throw new IllegalArgumentException("Invalid application id: " + applicationId); + } + this.applicationId = applicationId; + if (clientToken == null || clientToken.isEmpty()) { + throw new IllegalArgumentException("Invalid client token: " + clientToken); + } + this.clientToken = clientToken; + if (site == null || site.isEmpty()) { + this.site = DEFAULT_SITE; + } else if (validateSite(site)) { + this.site = site; + } else { + throw new IllegalArgumentException("Invalid site: " + site); + } + this.service = service; + this.env = env; + if (majorVersion != 5 && majorVersion != 6) { + throw new IllegalArgumentException("Invalid major version: " + majorVersion); + } + this.majorVersion = majorVersion; + this.version = version; + this.trackUserInteractions = trackUserInteractions; + this.trackResources = trackResources; + this.trackLongTask = trackLongTask; + if (sessionSampleRate != null && (sessionSampleRate < 0f || sessionSampleRate > 100f)) { + throw new IllegalArgumentException("Invalid session sample rate: " + sessionSampleRate); + } + this.sessionSampleRate = sessionSampleRate; + if (sessionReplaySampleRate != null + && (sessionReplaySampleRate < 0f || sessionReplaySampleRate > 100f)) { + throw new IllegalArgumentException( + "Invalid session replay sample rate: " + sessionReplaySampleRate); + } + this.sessionReplaySampleRate = sessionReplaySampleRate; + this.defaultPrivacyLevel = defaultPrivacyLevel; + } + + private static boolean validateSite(String site) { + for (String key : REGIONS.keySet()) { + if (key.equals(site)) { + return true; + } + } + return false; + } + + public String getSnippet() { + return "\n"; + } + + private String getCdnUrl() { + if (!GOV_CLOUD_SITE.equals(this.site)) { + return "https://www.datadoghq-browser-agent.com/datadog-rum-v" + this.majorVersion + ".js"; + } + return "https://www.datadoghq-browser-agent.com/" + + REGIONS.get(this.site) + + "/v" + + this.majorVersion + + "/datadog-rum.js"; + } + + private String jsonPayload() { + try (JsonWriter writer = new JsonWriter()) { + writer.beginObject(); + writer.name("application_id").value(this.applicationId); + writer.name("client_token").value(this.clientToken); + if (this.site != null) { + writer.name("site").value(this.site); + } + if (this.service != null) { + writer.name("service").value(this.service); + } + if (this.env != null) { + writer.name("env").value(this.env); + } + if (this.version != null) { + writer.name("version").value(this.version); + } + if (this.trackUserInteractions != null) { + writer.name("track_user_interactions").value(this.trackUserInteractions); + } + if (this.trackResources != null) { + writer.name("track_resources").value(this.trackResources); + } + if (this.trackLongTask != null) { + writer.name("track_long_task").value(this.trackLongTask); + } + if (this.defaultPrivacyLevel != null) { + writer.name("default_privacy_level").value(this.defaultPrivacyLevel.toJson()); + } + if (this.sessionSampleRate != null) { + writer.name("session_sample_rate").value(this.sessionSampleRate); + } + if (this.sessionReplaySampleRate != null) { + writer.name("session_replay_sample_rate").value(this.sessionReplaySampleRate); + } + return writer.toString(); + } catch (Exception e) { + throw new IllegalStateException("Fail to generate config payload", e); + } + } + + public enum PrivacyLevel { + ALLOW, + MASK, + MASK_USER_INPUT; + + public String toJson() { + return toString().toLowerCase(ROOT); + } + } +} 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 new file mode 100644 index 00000000000..b854c3ea804 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java @@ -0,0 +1,18 @@ +package datadog.trace.api.rum; + +public final class RumInjector { + + private static volatile String snippet; + + private static volatile RumConfig config; + + public static boolean isEnabled() { + // Check lazy config init + // TODO Config.isRumEnabled()? + valid config + return false; + } + + public static String getSnippet() { + return null; + } +} From 5e734e3a3e46387f9cecb536874e836dc0dbc266 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 06:59:17 +0200 Subject: [PATCH 02/13] feat(rum): Iterating on initial config and API --- .../datadog/trace/api/ConfigDefaults.java | 5 ++ .../datadog/trace/api/config/RumConfig.java | 20 ++++++ .../main/java/datadog/trace/api/Config.java | 68 ++++++++++++++++++- .../datadog/trace/api/rum/RumInjector.java | 36 ++++++++-- ...{RumConfig.java => RumInjectorConfig.java} | 33 +++------ 5 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java rename internal-api/src/main/java/datadog/trace/api/rum/{RumConfig.java => RumInjectorConfig.java} (87%) diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index a6d5888e76f..c3656cd3251 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -240,6 +240,11 @@ public final class ConfigDefaults { static final boolean DEFAULT_TELEMETRY_LOG_COLLECTION_ENABLED = true; static final int DEFAULT_TELEMETRY_DEPENDENCY_RESOLUTION_QUEUE_SIZE = 100000; + static final boolean DEFAULT_RUM_ENABLED = false; + static final int DEFAULT_RUM_MAJOR_VERSION = 6; + static final float DEFAULT_RUM_SESSION_SAMPLE_RATE = 100f; + static final float DEFAULT_RUM_SESSION_REPLAY_SAMPLE_RATE = 100f; + static final boolean DEFAULT_SSI_INJECTION_FORCE = false; static final String DEFAULT_INSTRUMENTATION_SOURCE = "manual"; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java new file mode 100644 index 00000000000..51d90fd31c4 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java @@ -0,0 +1,20 @@ +package datadog.trace.api.config; + +public final class RumConfig { + public static final String RUM_ENABLED = "rum.enabled"; + public static final String RUM_APPLICATION_ID = "rum.application.id"; + public static final String RUM_CLIENT_TOKEN = "rum.client.token"; + public static final String RUM_SITE = "rum.site"; + public static final String RUM_SERVICE = "rum.service"; + public static final String RUM_ENVIRONMENT = "rum.environment"; + public static final String RUM_MAJOR_VERSION = "rum.major.version"; + public static final String RUM_VERSION = "rum.version"; + public static final String RUM_TRACK_USER_INTERACTION = "rum.track.user.interaction"; + public static final String RUM_TRACK_RESOURCES = "rum.track.resources"; + public static final String RUM_TRACK_LONG_TASKS = "rum.track.long.tasks"; + public static final String RUM_DEFAULT_PRIVACY_LEVEL = "rum.default.privacy.level"; + public static final String RUM_SESSION_SAMPLE_RATE = "rum.session.sample.rate"; + public static final String RUM_SESSION_REPLAY_SAMPLE_RATE = "rum.session.replay.sample.rate"; + + private RumConfig() {} +} diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 3140a95d547..b40d229cf87 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -118,6 +118,10 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_POLL_INTERVAL_SECONDS; import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_TARGETS_KEY; import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_TARGETS_KEY_ID; +import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_ENABLED; +import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_MAJOR_VERSION; +import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_SESSION_REPLAY_SAMPLE_RATE; +import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_SESSION_SAMPLE_RATE; import static datadog.trace.api.ConfigDefaults.DEFAULT_SCOPE_DEPTH_LIMIT; import static datadog.trace.api.ConfigDefaults.DEFAULT_SCOPE_ITERATION_KEEP_ALIVE; import static datadog.trace.api.ConfigDefaults.DEFAULT_SECURE_RANDOM; @@ -461,6 +465,20 @@ import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_TARGETS_KEY; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_TARGETS_KEY_ID; import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_URL; +import static datadog.trace.api.config.RumConfig.RUM_APPLICATION_ID; +import static datadog.trace.api.config.RumConfig.RUM_CLIENT_TOKEN; +import static datadog.trace.api.config.RumConfig.RUM_DEFAULT_PRIVACY_LEVEL; +import static datadog.trace.api.config.RumConfig.RUM_ENABLED; +import static datadog.trace.api.config.RumConfig.RUM_ENVIRONMENT; +import static datadog.trace.api.config.RumConfig.RUM_MAJOR_VERSION; +import static datadog.trace.api.config.RumConfig.RUM_SERVICE; +import static datadog.trace.api.config.RumConfig.RUM_SESSION_REPLAY_SAMPLE_RATE; +import static datadog.trace.api.config.RumConfig.RUM_SESSION_SAMPLE_RATE; +import static datadog.trace.api.config.RumConfig.RUM_SITE; +import static datadog.trace.api.config.RumConfig.RUM_TRACK_LONG_TASKS; +import static datadog.trace.api.config.RumConfig.RUM_TRACK_RESOURCES; +import static datadog.trace.api.config.RumConfig.RUM_TRACK_USER_INTERACTION; +import static datadog.trace.api.config.RumConfig.RUM_VERSION; import static datadog.trace.api.config.TraceInstrumentationConfig.ADD_SPAN_POINTERS; import static datadog.trace.api.config.TraceInstrumentationConfig.AXIS_PROMOTE_RESOURCE_NAME; import static datadog.trace.api.config.TraceInstrumentationConfig.CASSANDRA_KEYSPACE_STATEMENT_EXTRACTION_ENABLED; @@ -618,6 +636,8 @@ import datadog.trace.api.iast.telemetry.Verbosity; import datadog.trace.api.naming.SpanNaming; import datadog.trace.api.profiling.ProfilingEnablement; +import datadog.trace.api.rum.RumInjectorConfig; +import datadog.trace.api.rum.RumInjectorConfig.PrivacyLevel; import datadog.trace.bootstrap.config.provider.CapturedEnvironmentConfigSource; import datadog.trace.bootstrap.config.provider.ConfigProvider; import datadog.trace.bootstrap.config.provider.SystemPropertiesConfigSource; @@ -1190,9 +1210,8 @@ public static String getHostName() { private final int stackTraceLengthLimit; - // RUM config -- start - // TODO - // RUM config -- end + private final boolean rumEnabled; + private final RumInjectorConfig rumInjectorConfig; // Read order: System Properties -> Env Variables, [-> properties file], [-> default value] private Config() { @@ -2677,9 +2696,40 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) this.stackTraceLengthLimit = configProvider.getInteger(STACK_TRACE_LENGTH_LIMIT, defaultStackTraceLengthLimit); + this.rumEnabled = configProvider.getBoolean(RUM_ENABLED, DEFAULT_RUM_ENABLED); + this.rumInjectorConfig = parseRumConfig(configProvider); + log.debug("New instance: {}", this); } + private static RumInjectorConfig parseRumConfig(ConfigProvider configProvider) { + String applicationId = configProvider.getString(RUM_APPLICATION_ID); + String clientToken = configProvider.getString(RUM_CLIENT_TOKEN); + if (applicationId == null || clientToken == null) { + return null; + } + try { + return new RumInjectorConfig( + applicationId, + clientToken, + configProvider.getString(RUM_SITE), + configProvider.getString(RUM_SERVICE), + configProvider.getString(RUM_ENVIRONMENT), + configProvider.getInteger(RUM_MAJOR_VERSION, DEFAULT_RUM_MAJOR_VERSION), + configProvider.getString(RUM_VERSION), + configProvider.getBoolean(RUM_TRACK_USER_INTERACTION), + configProvider.getBoolean(RUM_TRACK_RESOURCES), + configProvider.getBoolean(RUM_TRACK_LONG_TASKS), + configProvider.getEnum(RUM_DEFAULT_PRIVACY_LEVEL, PrivacyLevel.class, null), + configProvider.getFloat(RUM_SESSION_SAMPLE_RATE, DEFAULT_RUM_SESSION_SAMPLE_RATE), + configProvider.getFloat( + RUM_SESSION_REPLAY_SAMPLE_RATE, DEFAULT_RUM_SESSION_REPLAY_SAMPLE_RATE)); + } catch (IllegalArgumentException e) { + log.warn("Unable to configure RUM injection", e); + return null; + } + } + /** * Converts a list of packages in Jacoco exclusion format ({@code * my.package.*,my.other.package.*}) to list of package prefixes suitable for use with ASM ({@code @@ -4912,6 +4962,14 @@ public int getCloudPayloadTaggingMaxTags() { return cloudPayloadTaggingMaxTags; } + public boolean isRumEnabled() { + return this.rumEnabled; + } + + public RumInjectorConfig getRumInjectorConfig() { + return this.rumInjectorConfig; + } + private Set getSettingsSetFromEnvironment( String name, Function mapper, boolean splitOnWS) { final String value = configProvider.getString(name, ""); @@ -5592,6 +5650,10 @@ public String toString() { + cloudResponsePayloadTagging + ", experimentalPropagateProcessTagsEnabled=" + experimentalPropagateProcessTagsEnabled + + ", rumEnabled=" + + rumEnabled + + ", rumInjectorConfig=" + + rumInjectorConfig.jsonPayload() + '}'; } } 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 b854c3ea804..4b880f6a252 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 @@ -1,18 +1,40 @@ package datadog.trace.api.rum; -public final class RumInjector { +import datadog.trace.api.Config; +public final class RumInjector { + private static volatile boolean initialized = false; + private static volatile boolean enabled; private static volatile String snippet; - private static volatile RumConfig config; - + /** + * Check whether RUM injection is enabled and ready to inject. + * + * @return {@code true} if enabled, {@code otherwise}. + */ public static boolean isEnabled() { - // Check lazy config init - // TODO Config.isRumEnabled()? + valid config - return false; + if (!initialized) { + Config config = Config.get(); + boolean rumEnabled = config.isRumEnabled(); + RumInjectorConfig injectorConfig = config.getRumInjectorConfig(); + if (rumEnabled && injectorConfig != null) { + enabled = true; + snippet = injectorConfig.getSnippet(); + } else { + enabled = false; + snippet = null; + } + initialized = true; + } + return enabled; } + /** + * Get the HTML snippet to inject RUM SDK + * + * @return The HTML snippet to inject, {@code null} if RUM injection is disabled to inject. + */ public static String getSnippet() { - return null; + return snippet; } } diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorConfig.java similarity index 87% rename from internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java rename to internal-api/src/main/java/datadog/trace/api/rum/RumInjectorConfig.java index f9e241f7bca..66b350c2b46 100644 --- a/internal-api/src/main/java/datadog/trace/api/rum/RumConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorConfig.java @@ -3,12 +3,11 @@ import static java.util.Locale.ROOT; import datadog.json.JsonWriter; -import datadog.trace.api.Config; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; -public class RumConfig { +public class RumInjectorConfig { private static final String DEFAULT_SITE = "datadoghq.com"; private static final String GOV_CLOUD_SITE = "ddog-gov.com"; private static final Map REGIONS = new HashMap<>(); @@ -44,19 +43,14 @@ public class RumConfig { /** The privacy level for data collection. */ @Nullable public final PrivacyLevel defaultPrivacyLevel; /** The percentage of user sessions to be tracked (between 0.0 and 100.0). */ - @Nullable public final Float sessionSampleRate; + public final float sessionSampleRate; /** * The percentage of tracked sessions that will include Session Replay data (between 0.0 and * 100.0). */ - @Nullable public final Float sessionReplaySampleRate; + public final float sessionReplaySampleRate; - public static RumConfig from(Config config) { - // TODO - return null; - } - - RumConfig( + public RumInjectorConfig( String applicationId, String clientToken, @Nullable String site, @@ -68,8 +62,8 @@ public static RumConfig from(Config config) { @Nullable Boolean trackResources, @Nullable Boolean trackLongTask, @Nullable PrivacyLevel defaultPrivacyLevel, - @Nullable Float sessionSampleRate, - @Nullable Float sessionReplaySampleRate) { + float sessionSampleRate, + float sessionReplaySampleRate) { if (applicationId == null || applicationId.isEmpty()) { throw new IllegalArgumentException("Invalid application id: " + applicationId); } @@ -95,12 +89,11 @@ public static RumConfig from(Config config) { this.trackUserInteractions = trackUserInteractions; this.trackResources = trackResources; this.trackLongTask = trackLongTask; - if (sessionSampleRate != null && (sessionSampleRate < 0f || sessionSampleRate > 100f)) { + if (sessionSampleRate < 0f || sessionSampleRate > 100f) { throw new IllegalArgumentException("Invalid session sample rate: " + sessionSampleRate); } this.sessionSampleRate = sessionSampleRate; - if (sessionReplaySampleRate != null - && (sessionReplaySampleRate < 0f || sessionReplaySampleRate > 100f)) { + if (sessionReplaySampleRate < 0f || sessionReplaySampleRate > 100f) { throw new IllegalArgumentException( "Invalid session replay sample rate: " + sessionReplaySampleRate); } @@ -145,7 +138,7 @@ private String getCdnUrl() { + "/datadog-rum.js"; } - private String jsonPayload() { + public String jsonPayload() { try (JsonWriter writer = new JsonWriter()) { writer.beginObject(); writer.name("application_id").value(this.applicationId); @@ -174,12 +167,8 @@ private String jsonPayload() { if (this.defaultPrivacyLevel != null) { writer.name("default_privacy_level").value(this.defaultPrivacyLevel.toJson()); } - if (this.sessionSampleRate != null) { - writer.name("session_sample_rate").value(this.sessionSampleRate); - } - if (this.sessionReplaySampleRate != null) { - writer.name("session_replay_sample_rate").value(this.sessionReplaySampleRate); - } + writer.name("session_sample_rate").value(this.sessionSampleRate); + writer.name("session_replay_sample_rate").value(this.sessionReplaySampleRate); return writer.toString(); } catch (Exception e) { throw new IllegalStateException("Fail to generate config payload", e); From dde7164cb6eb557a08ed832719dfab0697bd2c79 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 07:46:51 +0200 Subject: [PATCH 03/13] fix(rum): Fix config print --- internal-api/src/main/java/datadog/trace/api/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index b40d229cf87..56a55be4204 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -5653,7 +5653,7 @@ public String toString() { + ", rumEnabled=" + rumEnabled + ", rumInjectorConfig=" - + rumInjectorConfig.jsonPayload() + + (rumInjectorConfig == null ? "null" : rumInjectorConfig.jsonPayload()) + '}'; } } From bb22e1ac0cfac43044708a429f1a3a0b7ea605b6 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 08:32:45 +0200 Subject: [PATCH 04/13] fix(rum): Remove content type support as only "text/html" is supported --- .../trace/api/rum/ContentTypesCache.java | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java diff --git a/internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java b/internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java deleted file mode 100644 index 59c439c7173..00000000000 --- a/internal-api/src/main/java/datadog/trace/api/rum/ContentTypesCache.java +++ /dev/null @@ -1,45 +0,0 @@ -package datadog.trace.api.rum; - -import java.util.HashMap; -import java.util.Map; - -/* - * Cache for ContentTypes answers - */ -public class ContentTypesCache { - static final short MAX_ITEMS = 1_000; - private final Map map; - - /* - * Constructs a new ContentTypes cache for the provided types - * - * @param contentTypes the list of ContentTypes it will store answers for - */ - public ContentTypesCache(String[] contentTypes) { - this.map = new HashMap<>(); - for (String contentType : contentTypes) { - this.map.put(contentType, true); - } - } - - public boolean contains(String headerValue) { - return this.map.computeIfAbsent(headerValue, this::shouldProcess); - } - - private boolean shouldProcess(String headerValue) { - Boolean typeAllowed = this.map.get(getContentType(headerValue)); - return typeAllowed != null && typeAllowed; - } - - private String getContentType(String headerValue) { - // multipart/* are expected to contain boundary unique values - // let's abort early to avoid exploding the cache - // additionally, if the cache is already too big, let's also abort - if (this.map.size() > MAX_ITEMS || headerValue.startsWith("multipart/")) { - return null; - } - // RFC 2045 defines optional parameters always behind a semicolon - int semicolon = headerValue.indexOf(";"); - return semicolon != -1 ? headerValue.substring(0, semicolon) : headerValue; - } -} From 1bf99a1d1848566c95880828ed140d47766dc840 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 10:17:39 +0200 Subject: [PATCH 05/13] feat(rum): Add smoke tests --- dd-smoke-tests/rum/build.gradle | 7 +++ .../rum/AbstractRumServerSmokeTest.groovy | 38 ++++++++++++++++ dd-smoke-tests/rum/tomcat-9/build.gradle | 26 +++++++++++ .../main/java/com/example/HelloServlet.java | 28 ++++++++++++ .../src/main/java/com/example/Main.java | 34 +++++++++++++++ .../rum/tomcat9/Tomcat9RumSmokeTest.groovy | 43 +++++++++++++++++++ settings.gradle | 2 + 7 files changed, 178 insertions(+) create mode 100644 dd-smoke-tests/rum/build.gradle create mode 100644 dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy create mode 100644 dd-smoke-tests/rum/tomcat-9/build.gradle create mode 100644 dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java create mode 100644 dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java create mode 100644 dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy diff --git a/dd-smoke-tests/rum/build.gradle b/dd-smoke-tests/rum/build.gradle new file mode 100644 index 00000000000..f5a54e7985a --- /dev/null +++ b/dd-smoke-tests/rum/build.gradle @@ -0,0 +1,7 @@ +apply from: "$rootDir/gradle/java.gradle" + +description = 'appsec-smoke-tests' + +dependencies { + api project(':dd-smoke-tests') +} diff --git a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy new file mode 100644 index 00000000000..6fa39dcf792 --- /dev/null +++ b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy @@ -0,0 +1,38 @@ +package datadog.smoketest.rum + +import datadog.smoketest.AbstractServerSmokeTest +import okhttp3.Response +import spock.lang.Shared + +class AbstractRumServerSmokeTest extends AbstractServerSmokeTest { + + + + @Shared + protected String[] defaultRumProperties = [ + "-Ddd.appsec.enabled=${System.getProperty('smoke_test.appsec.enabled') ?: 'true'}", + "-Ddd.profiling.enabled=false", + // TODO: Remove once this is the default value + "-Ddd.api-security.enabled=true", + "-Ddd.appsec.waf.timeout=300000", + "-DPOWERWAF_EXIT_ON_LEAK=true", + // disable AppSec rate limit + "-Ddd.appsec.trace.rate.limit=-1" + ] + (System.getProperty('smoke_test.appsec.enabled') == 'inactive' ? + // enable remote config so that appsec is partially enabled (rc is now enabled by default) + [ + '-Ddd.remote_config.url=https://127.0.0.1:54670/invalid_endpoint', + '-Ddd.remote_config.poll_interval.seconds=3600' + ]: + ['-Ddd.remote_config.enabled=false'] + ) + + + static void assertRumInjected(Response response) { + assert response.header('x-datadog-rum-injected') == '1' : 'RUM injected header missing' + def content = response.body().toString() + assert content.contains('https://www.datadoghq-browser-agent.com') : 'RUM script not injected' + } + + +} diff --git a/dd-smoke-tests/rum/tomcat-9/build.gradle b/dd-smoke-tests/rum/tomcat-9/build.gradle new file mode 100644 index 00000000000..2c4d856c541 --- /dev/null +++ b/dd-smoke-tests/rum/tomcat-9/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'com.gradleup.shadow' +} + +apply from: "$rootDir/gradle/java.gradle" +description = 'RUM Tomcat 9 Smoke Tests' + +dependencies { + implementation 'org.apache.tomcat.embed:tomcat-embed-core:9.0.88' + implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.88' + implementation 'javax.servlet:javax.servlet-api:4.0.1' + + testImplementation project(':dd-smoke-tests:rum') +} + +jar { + manifest { + attributes('Main-Class': 'com.example.Main') + } +} + +tasks.withType(Test).configureEach { + dependsOn "shadowJar" + + jvmArgs "-Ddatadog.smoketest.rum.tomcat9.shadowJar.path=${tasks.shadowJar.archiveFile.get()}" +} diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java new file mode 100644 index 00000000000..e86b58e65c2 --- /dev/null +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java @@ -0,0 +1,28 @@ +package com.example; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class HelloServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/html;charset=UTF-8"); + resp.getWriter().write( + "" + + "" + + "" + + " " + + " " + + " Hello Servlet" + + "" + + "" + + "

Hello from Tomcat 9 Servlet!

" + + "

This is a demo HTML page served by Java servlet.

" + + "" + + "" + ); + } +} diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java new file mode 100644 index 00000000000..ce614550a82 --- /dev/null +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java @@ -0,0 +1,34 @@ +package com.example; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; + +import java.io.File; + +public class Main { + public static void main(String[] args) throws Exception { + int port = 8080; + if (args.length == 1) { + port = Integer.parseInt(args[0]); + } + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(port); + + // Setup base directory + tomcat.setBaseDir("."); + + // Add webapp context + String contextPath = "/"; + String docBase = new File(".").getAbsolutePath(); + Context context = tomcat.addContext(contextPath, docBase); + + // Add servlet programmatically + context.addServletContainerInitializer((c, ctx) -> { + ctx.addServlet("helloServlet", new HelloServlet()).addMapping("/hello"); + }, null); + + tomcat.start(); + tomcat.getServer().await(); + } +} diff --git a/dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy b/dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy new file mode 100644 index 00000000000..25bfe66de87 --- /dev/null +++ b/dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy @@ -0,0 +1,43 @@ +package datadog.smoketest.rum.tomcat9 + +import datadog.smoketest.rum.AbstractRumServerSmokeTest +import datadog.trace.api.Platform +import okhttp3.Request +import okhttp3.Response + +class Tomcat9RumSmokeTest extends AbstractRumServerSmokeTest { + + + @Override + ProcessBuilder createProcessBuilder() { + String jarPath = System.getProperty('datadog.smoketest.rum.tomcat9.shadowJar.path') + + List command = [] + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(defaultRumProperties) + if (Platform.isJavaVersionAtLeast(17)) { + command.addAll((String[]) ['--add-opens', 'java.base/java.lang=ALL-UNNAMED']) + } + command.addAll(['-jar', jarPath, Integer.toString(httpPort)]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + return processBuilder + } + + void 'test RUM SDK injection'() { + given: + def url = "http://localhost:${httpPort}/hello" + def request = new Request.Builder() + .url(url) + .get() + .build() + + when: + Response response = client.newCall(request).execute() + + then: + response.code() == 200 + assertRumInjected(response) + } +} diff --git a/settings.gradle b/settings.gradle index cc17a3de79f..c414abaf11e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -153,6 +153,8 @@ include ':dd-smoke-tests:quarkus-native' include ':dd-smoke-tests:sample-trace' include ':dd-smoke-tests:ratpack-1.5' include ':dd-smoke-tests:resteasy' +include ':dd-smoke-tests:rum' +include ':dd-smoke-tests:rum:tomcat-9' include ':dd-smoke-tests:spring-boot-3.0-native' include ':dd-smoke-tests:spring-boot-2.4-webflux' include ':dd-smoke-tests:spring-boot-2.5-webflux' From 8814d867cbe195e37bf6630e52edc6de7c291f6a Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 11:26:39 +0200 Subject: [PATCH 06/13] feat(rum): Add smoke tests --- .../rum/AbstractRumServerSmokeTest.groovy | 41 ++++++------------- .../main/java/com/example/HelloServlet.java | 3 +- .../src/main/java/com/example/Main.java | 11 +++-- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy index 6fa39dcf792..9144ccdde39 100644 --- a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy +++ b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy @@ -5,34 +5,19 @@ import okhttp3.Response import spock.lang.Shared class AbstractRumServerSmokeTest extends AbstractServerSmokeTest { - - - - @Shared - protected String[] defaultRumProperties = [ - "-Ddd.appsec.enabled=${System.getProperty('smoke_test.appsec.enabled') ?: 'true'}", - "-Ddd.profiling.enabled=false", - // TODO: Remove once this is the default value - "-Ddd.api-security.enabled=true", - "-Ddd.appsec.waf.timeout=300000", - "-DPOWERWAF_EXIT_ON_LEAK=true", - // disable AppSec rate limit - "-Ddd.appsec.trace.rate.limit=-1" - ] + (System.getProperty('smoke_test.appsec.enabled') == 'inactive' ? - // enable remote config so that appsec is partially enabled (rc is now enabled by default) - [ - '-Ddd.remote_config.url=https://127.0.0.1:54670/invalid_endpoint', - '-Ddd.remote_config.poll_interval.seconds=3600' - ]: - ['-Ddd.remote_config.enabled=false'] - ) - - - static void assertRumInjected(Response response) { - assert response.header('x-datadog-rum-injected') == '1' : 'RUM injected header missing' - def content = response.body().toString() - assert content.contains('https://www.datadoghq-browser-agent.com') : 'RUM script not injected' - } + @Shared + protected String[] defaultRumProperties = [ + "-Ddd.rum.enabled=true", + "-Ddd.rum.application.id=appid", + "-Ddd.rum.client.token=token" + ] + + + static void assertRumInjected(Response response) { + assert response.header('x-datadog-rum-injected') == '1': 'RUM injected header missing' + def content = response.body().toString() + assert content.contains('https://www.datadoghq-browser-agent.com'): 'RUM script not injected' + } } diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java index e86b58e65c2..dff7dec2ebc 100644 --- a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java @@ -1,6 +1,5 @@ package com.example; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -8,7 +7,7 @@ public class HelloServlet extends HttpServlet { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write( "" + diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java index ce614550a82..434bf83efa5 100644 --- a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java @@ -1,12 +1,12 @@ package com.example; +import java.io.File; import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; -import java.io.File; - public class Main { - public static void main(String[] args) throws Exception { + public static void main(String[] args) throws LifecycleException { int port = 8080; if (args.length == 1) { port = Integer.parseInt(args[0]); @@ -14,12 +14,11 @@ public static void main(String[] args) throws Exception { Tomcat tomcat = new Tomcat(); tomcat.setPort(port); - - // Setup base directory + tomcat.getConnector(); // This is required to make Tomcat start tomcat.setBaseDir("."); // Add webapp context - String contextPath = "/"; + String contextPath = ""; String docBase = new File(".").getAbsolutePath(); Context context = tomcat.addContext(contextPath, docBase); From a1f0a77a943a865f41e33e2ab49634ca70caf7d3 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 11:27:49 +0200 Subject: [PATCH 07/13] feat(rum): Add smoke tests --- .../main/java/com/example/HelloServlet.java | 38 +++++++++---------- .../src/main/java/com/example/Main.java | 8 ++-- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java index dff7dec2ebc..d88c4774078 100644 --- a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java @@ -1,27 +1,27 @@ package com.example; +import java.io.IOException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; public class HelloServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType("text/html;charset=UTF-8"); - resp.getWriter().write( - "" + - "" + - "" + - " " + - " " + - " Hello Servlet" + - "" + - "" + - "

Hello from Tomcat 9 Servlet!

" + - "

This is a demo HTML page served by Java servlet.

" + - "" + - "" - ); - } + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/html;charset=UTF-8"); + resp.getWriter() + .write( + "" + + "" + + "" + + " " + + " " + + " Hello Servlet" + + "" + + "" + + "

Hello from Tomcat 9 Servlet!

" + + "

This is a demo HTML page served by Java servlet.

" + + "" + + ""); + } } diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java index 434bf83efa5..a69a8701d89 100644 --- a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java @@ -23,9 +23,11 @@ public static void main(String[] args) throws LifecycleException { Context context = tomcat.addContext(contextPath, docBase); // Add servlet programmatically - context.addServletContainerInitializer((c, ctx) -> { - ctx.addServlet("helloServlet", new HelloServlet()).addMapping("/hello"); - }, null); + context.addServletContainerInitializer( + (c, ctx) -> { + ctx.addServlet("helloServlet", new HelloServlet()).addMapping("/hello"); + }, + null); tomcat.start(); tomcat.getServer().await(); From f941b4a69bc354486ac07d11e005d75620f4b2e9 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 4 Jul 2025 11:29:02 +0200 Subject: [PATCH 08/13] feat(rum): Add smoke tests --- .../rum/AbstractRumServerSmokeTest.groovy | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy index 9144ccdde39..27d2b03ecdf 100644 --- a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy +++ b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy @@ -5,19 +5,17 @@ import okhttp3.Response import spock.lang.Shared class AbstractRumServerSmokeTest extends AbstractServerSmokeTest { - @Shared - protected String[] defaultRumProperties = [ - "-Ddd.rum.enabled=true", - "-Ddd.rum.application.id=appid", - "-Ddd.rum.client.token=token" - ] - - - static void assertRumInjected(Response response) { - assert response.header('x-datadog-rum-injected') == '1': 'RUM injected header missing' - def content = response.body().toString() - assert content.contains('https://www.datadoghq-browser-agent.com'): 'RUM script not injected' - } + @Shared + protected String[] defaultRumProperties = [ + "-Ddd.rum.enabled=true", + "-Ddd.rum.application.id=appid", + "-Ddd.rum.client.token=token" + ] + static void assertRumInjected(Response response) { + assert response.header('x-datadog-rum-injected') == '1': 'RUM injected header missing' + def content = response.body().toString() + assert content.contains('https://www.datadoghq-browser-agent.com'): 'RUM script not injected' + } } From fe2006cd7c653e293498be8eded1ca9132106483 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 4 Jul 2025 11:56:28 +0200 Subject: [PATCH 09/13] Add RUM injection for servlet 3 --- .../buffer/InjectingPipeOutputStream.java | 90 +++++++++++++++++++ .../decorator/HttpServerDecorator.java | 1 + .../InjectingPipeOutputStreamTest.groovy | 23 +++++ .../RumHttpServletResponseWrapper.java | 84 +++++++++++++++++ .../servlet3/Servlet3Advice.java | 12 ++- .../servlet3/Servlet3Instrumentation.java | 2 + .../servlet3/WrappedServletOutputStream.java | 57 ++++++++++++ .../src/test/groovy/JettyServlet3Test.groovy | 86 ++++++++++++++++-- .../datadog/trace/api/rum/RumInjector.java | 37 +++++++- 9 files changed, 382 insertions(+), 10 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java create mode 100644 dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy create mode 100644 dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java create mode 100644 dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java 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 new file mode 100644 index 00000000000..f5d7d60404d --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -0,0 +1,90 @@ +package datadog.trace.bootstrap.instrumentation.buffer; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.function.Consumer; + +/** + * A circular buffer that holds n+1 bytes and with a lookbehind buffer of n bytes. The first time + * that the latest n bytes matches the marker, a content is injected before. + */ +public class InjectingPipeOutputStream extends FilterOutputStream { + private final byte[] lookbehind; + private int pos; + private boolean bufferFilled; + private final byte[] marker; + private final byte[] contentToInject; + private boolean found = false; + private int matchingPos = 0; + private final Consumer onContentInjected; + + /** + * @param downstream the delegate output stream + * @param marker the marker to find in the stream + * @param contentToInject the content to inject once before the marker if found. + */ + public InjectingPipeOutputStream( + final OutputStream downstream, + final byte[] marker, + final byte[] contentToInject, + final Consumer onContentInjected) { + super(downstream); + this.marker = marker; + this.lookbehind = new byte[marker.length + 1]; + this.pos = 0; + this.contentToInject = contentToInject; + this.onContentInjected = onContentInjected; + } + + @Override + public void write(int b) throws IOException { + if (found) { + out.write(b); + return; + } + lookbehind[pos] = (byte) b; + pos = (pos + 1) % lookbehind.length; + + if (marker[matchingPos++] == b) { + if (matchingPos == marker.length) { + found = true; + out.write(contentToInject); + if (onContentInjected != null) { + onContentInjected.accept(null); + } + drain((pos + 1) % lookbehind.length, marker.length); + return; + } + } else { + matchingPos = 0; + } + + if (!bufferFilled) { + bufferFilled = pos == lookbehind.length - 1; + } + + if (bufferFilled) { + super.write(lookbehind[(pos + 1) % lookbehind.length]); + } + } + + private void drain(int from, int size) throws IOException { + while (size-- > 0) { + super.write(Character.valueOf((char) lookbehind[from])); + from = (from + 1) % lookbehind.length; + } + } + + @Override + public void close() throws IOException { + if (!found) { + if (bufferFilled) { + drain((pos + 2) % lookbehind.length, marker.length - 1); + } else { + drain(0, pos); + } + } + super.close(); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java index 33e2ed2ebd8..fa712db6ceb 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java @@ -57,6 +57,7 @@ public abstract class HttpServerDecorator" | "" | "" | true | "" + "" | "" | "" | false | "" + "" | "" | "" | false | "" + } +} 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 new file mode 100644 index 00000000000..389bf77eddf --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java @@ -0,0 +1,84 @@ +package datadog.trace.instrumentation.servlet3; + +import datadog.trace.api.rum.RumInjector; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +public class RumHttpServletResponseWrapper extends HttpServletResponseWrapper { + private ServletOutputStream outputStream; + private PrintWriter printWriter; + private boolean shouldInject; + + public RumHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (!shouldInject) { + return super.getOutputStream(); + } + if (outputStream == null) { + String encoding = getCharacterEncoding(); + if (encoding == null) { + encoding = "UTF-8"; + } + outputStream = + new WrappedServletOutputStream( + super.getOutputStream(), + RumInjector.getMarker(encoding), + RumInjector.getSnippet(encoding), + this::onInjected); + } + return outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (!shouldInject) { + return super.getWriter(); + } + if (printWriter == null) { + printWriter = new PrintWriter(getOutputStream()); + } + return printWriter; + } + + @Override + public void setContentLength(int len) { + // don't set it since we don't know if we will inject + } + + @Override + public void reset() { + this.outputStream = null; + this.printWriter = null; + this.shouldInject = false; + super.reset(); + } + + @Override + public void resetBuffer() { + this.outputStream = null; + this.printWriter = null; + this.shouldInject = false; + super.resetBuffer(); + } + + public void onInjected(Void ignored) { + try { + setHeader("x-datadog-rum-injected", "1"); + } catch (Throwable ignored2) { + } + } + + @Override + public void setContentType(String type) { + if (type != null && type.contains("html")) { + shouldInject = true; + } + } +} 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 b4f1a5ad164..8ed6322fe4d 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 @@ -4,6 +4,7 @@ import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.spanFromContext; import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_DISPATCH_SPAN_ATTRIBUTE; import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_FIN_DISP_LIST_SPAN_ATTRIBUTE; +import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_RUM_INJECTED; import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_SPAN_ATTRIBUTE; import static datadog.trace.instrumentation.servlet3.Servlet3Decorator.DECORATE; @@ -15,6 +16,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.GlobalTracer; import datadog.trace.api.gateway.Flow; +import datadog.trace.api.rum.RumInjector; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.instrumentation.servlet.ServletBlockingHelper; import java.security.Principal; @@ -30,7 +32,7 @@ public class Servlet3Advice { @Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class) public static boolean onEnter( @Advice.Argument(value = 0, readOnly = false) ServletRequest request, - @Advice.Argument(value = 1) ServletResponse response, + @Advice.Argument(value = 1, readOnly = false) ServletResponse response, @Advice.Local("isDispatch") boolean isDispatch, @Advice.Local("finishSpan") boolean finishSpan, @Advice.Local("contextScope") ContextScope scope) { @@ -41,7 +43,13 @@ public static boolean onEnter( } final HttpServletRequest httpServletRequest = (HttpServletRequest) request; - final HttpServletResponse httpServletResponse = (HttpServletResponse) response; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + + if (RumInjector.isEnabled() && httpServletRequest.getAttribute(DD_RUM_INJECTED) == null) { + httpServletRequest.setAttribute(DD_RUM_INJECTED, Boolean.TRUE); + httpServletResponse = new RumHttpServletResponseWrapper(httpServletResponse); + response = httpServletResponse; + } Object dispatchSpan = request.getAttribute(DD_DISPATCH_SPAN_ATTRIBUTE); if (dispatchSpan instanceof AgentSpan) { diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java index a7dc4f28368..d7732045329 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Instrumentation.java @@ -52,6 +52,8 @@ public String[] helperClassNames() { packageName + ".Servlet3Decorator", packageName + ".ServletRequestURIAdapter", packageName + ".FinishAsyncDispatchListener", + packageName + ".RumHttpServletResponseWrapper", + packageName + ".WrappedServletOutputStream", "datadog.trace.instrumentation.servlet.ServletBlockingHelper", }; } 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 new file mode 100644 index 00000000000..fbafb01cdc0 --- /dev/null +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/WrappedServletOutputStream.java @@ -0,0 +1,57 @@ +package datadog.trace.instrumentation.servlet3; + +import datadog.trace.bootstrap.instrumentation.buffer.InjectingPipeOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.function.Consumer; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; + +public class WrappedServletOutputStream extends ServletOutputStream { + private final OutputStream filtered; + private final ServletOutputStream delegate; + + public WrappedServletOutputStream( + ServletOutputStream delegate, + byte[] marker, + byte[] contentToInject, + Consumer onInjected) { + this.filtered = new InjectingPipeOutputStream(delegate, marker, contentToInject, onInjected); + this.delegate = delegate; + } + + @Override + public void write(int b) throws IOException { + filtered.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + filtered.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + filtered.write(b, off, len); + } + + @Override + public void flush() throws IOException { + filtered.flush(); + } + + @Override + public void close() throws IOException { + filtered.close(); + } + + @Override + public boolean isReady() { + return delegate.isReady(); + } + + @Override + public void setWriteListener(WriteListener writeListener) { + delegate.setWriteListener(writeListener); + } +} diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/JettyServlet3Test.groovy b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/JettyServlet3Test.groovy index 61a255e1f0d..2ed349c5402 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/JettyServlet3Test.groovy +++ b/dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/JettyServlet3Test.groovy @@ -3,6 +3,7 @@ import datadog.trace.agent.test.base.HttpServer import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.ApplicationModule +import datadog.trace.api.rum.RumInjector import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.instrumentation.servlet3.AsyncDispatcherDecorator @@ -20,6 +21,7 @@ import javax.servlet.AsyncListener import javax.servlet.Servlet import javax.servlet.ServletException import javax.servlet.annotation.WebServlet +import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -167,8 +169,8 @@ abstract class JettyServlet3Test extends AbstractServlet3Test expectedExtraErrorInformation(ServerEndpoint endpoint) { if (endpoint.throwsException) { ["error.message": "${endpoint.body}", - "error.type": { it == Exception.name || it == InputMismatchException.name }, - "error.stack": String] + "error.type" : { it == Exception.name || it == InputMismatchException.name }, + "error.stack" : String] } else { Collections.emptyMap() } @@ -233,6 +235,77 @@ class JettyServlet3TestSync extends JettyServlet3Test { } } +class JettyServlet3SyncRumInjectionForkedTest extends JettyServlet3TestSync { + + static class RumServlet extends HttpServlet { + private final String mimeType + + RumServlet(String mime) { + this.mimeType = mime + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType(mimeType) + try (def writer = resp.getWriter()) { + writer.println("\n" + + "\n" + + "\n" + + " \n" + + " This is the title of the webpage!\n" + + " \n" + + " \n" + + "

This is an example paragraph. Anything in the body tag will appear on the page, just like this p tag and its contents.

\n" + + " \n" + + "") + } + } + } + + static class HtmlRumServlet extends RumServlet { + HtmlRumServlet() { + super("text/html") + } + } + + static class XmlRumServlet extends RumServlet { + XmlRumServlet() { + super("text/xml") + } + } + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("rum.enabled", "true") + injectSysConfig("rum.application.id", "test") + injectSysConfig("rum.client.token", "secret") + } + + @Override + protected void setupServlets(ServletContextHandler servletContextHandler) { + super.setupServlets(servletContextHandler) + addServlet(servletContextHandler, "/gimme-html", HtmlRumServlet) + addServlet(servletContextHandler, "/gimme-xml", XmlRumServlet) + } + + def "test rum injection in head for mime #mime"() { + setup: + def request = new okhttp3.Request.Builder().url(server.address().resolve("gimme-$mime").toURL()) + .get().build() + when: + def response = client.newCall(request).execute() + then: + assert response.code() == 200 + assert response.body().string().contains(new String(RumInjector.getSnippet("UTF-8"), "UTF-8")) == expected + assert response.header("x-datadog-rum-injected") == (expected ? "1" : null) + where: + mime | expected + "html" | true + "xml" | false + } +} + class JettyServlet3SyncV1ForkedTest extends JettyServlet3TestSync implements TestingGenericHttpNamingConventions.ServerV1 { @@ -260,6 +333,7 @@ class JettyServlet3TestAsync extends JettyServlet3Test { class JettyServlet3ASyncV1ForkedTest extends JettyServlet3TestAsync implements TestingGenericHttpNamingConventions.ServerV1 { } + class JettyServlet3TestFakeAsync extends JettyServlet3Test { @Override @@ -586,16 +660,16 @@ class IastJettyServlet3ForkedTest extends JettyServlet3TestSync { client.newCall(request).execute() then: - 1 * appModule.onRealPath(_) - 1 * appModule.checkSessionTrackingModes(_) + 1 * appModule.onRealPath(_) + 1 * appModule.checkSessionTrackingModes(_) 0 * _ when: client.newCall(request).execute() then: //Only call once per application context - 0 * appModule.onRealPath(_) - 0 * appModule.checkSessionTrackingModes(_) + 0 * appModule.onRealPath(_) + 0 * appModule.checkSessionTrackingModes(_) 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 4b880f6a252..25e337f67ec 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 @@ -1,12 +1,35 @@ package datadog.trace.api.rum; import datadog.trace.api.Config; +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; +import java.util.function.Function; public final class RumInjector { private static volatile boolean initialized = false; private static volatile boolean enabled; private static volatile String snippet; + private static final String MARKER = ""; + private static final DDCache SNIPPET_CACHE = DDCaches.newFixedSizeCache(16); + private static final DDCache MARKER_CACHE = DDCaches.newFixedSizeCache(16); + private static final Function SNIPPET_ADDER = + charset -> { + try { + return snippet.getBytes(charset); + } catch (Throwable t) { + return null; + } + }; + private static final Function MARKER_ADDER = + charset -> { + try { + return MARKER.getBytes(charset); + } catch (Throwable t) { + return null; + } + }; + /** * Check whether RUM injection is enabled and ready to inject. * @@ -34,7 +57,17 @@ public static boolean isEnabled() { * * @return The HTML snippet to inject, {@code null} if RUM injection is disabled to inject. */ - public static String getSnippet() { - return snippet; + public static byte[] getSnippet(String encoding) { + if (!isEnabled()) { + return null; + } + return SNIPPET_CACHE.computeIfAbsent(encoding, SNIPPET_ADDER); + } + + public static byte[] getMarker(String encoding) { + if (!isEnabled()) { + return null; + } + return MARKER_CACHE.computeIfAbsent(encoding, MARKER_ADDER); } } From 2ac167a7eac2f54d01df0f91385d28e0b0442f86 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 4 Jul 2025 12:11:12 +0200 Subject: [PATCH 10/13] fix rum injection smoke test --- .../rum/AbstractRumServerSmokeTest.groovy | 2 +- .../main/java/com/example/HelloServlet.java | 30 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy index 27d2b03ecdf..dade3311a41 100644 --- a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy +++ b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy @@ -15,7 +15,7 @@ class AbstractRumServerSmokeTest extends AbstractServerSmokeTest { static void assertRumInjected(Response response) { assert response.header('x-datadog-rum-injected') == '1': 'RUM injected header missing' - def content = response.body().toString() + def content = response.body().string() assert content.contains('https://www.datadoghq-browser-agent.com'): 'RUM script not injected' } } diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java index d88c4774078..5639cb65677 100644 --- a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java +++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java @@ -1,6 +1,7 @@ package com.example; import java.io.IOException; +import java.io.PrintWriter; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -9,19 +10,20 @@ public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html;charset=UTF-8"); - resp.getWriter() - .write( - "" - + "" - + "" - + " " - + " " - + " Hello Servlet" - + "" - + "" - + "

Hello from Tomcat 9 Servlet!

" - + "

This is a demo HTML page served by Java servlet.

" - + "" - + ""); + try (final PrintWriter writer = resp.getWriter()) { + writer.write( + "" + + "" + + "" + + " " + + " " + + " Hello Servlet" + + "" + + "" + + "

Hello from Tomcat 9 Servlet!

" + + "

This is a demo HTML page served by Java servlet.

" + + "" + + ""); + } } } From c389b7767fdcc597dd17d25246da4babf0b8be3f Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 4 Jul 2025 12:26:43 +0200 Subject: [PATCH 11/13] fix javadoc --- .../instrumentation/buffer/InjectingPipeOutputStream.java | 1 + 1 file changed, 1 insertion(+) 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 f5d7d60404d..ea3322103f7 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 @@ -23,6 +23,7 @@ public class InjectingPipeOutputStream extends FilterOutputStream { * @param downstream the delegate output stream * @param marker the marker to find in the stream * @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, From 01f6aaeee17956416184ab513e27ad4417a6509f Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 4 Jul 2025 13:53:30 +0200 Subject: [PATCH 12/13] Add benchmark --- .../InjectingPipeOutputStreamBenchmark.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java 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 new file mode 100644 index 00000000000..2432469949c --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamBenchmark.java @@ -0,0 +1,60 @@ +package datadog.trace.bootstrap.instrumentation.buffer; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.commons.io.IOUtils; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +@Warmup(iterations = 1, time = 30, timeUnit = SECONDS) +@Measurement(iterations = 2, time = 30, timeUnit = SECONDS) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 1) +public class InjectingPipeOutputStreamBenchmark { + private static final List htmlContent; + private static final byte[] marker; + private static final byte[] content; + + static { + try (InputStream is = new URL("https://www.google.com").openStream()) { + htmlContent = IOUtils.readLines(is, StandardCharsets.UTF_8); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + marker = "".getBytes(StandardCharsets.UTF_8); + content = "