Skip to content

Commit 2b95dcf

Browse files
Implement Bazel support for CI Visibility (#11150)
# What Does This Do Adds Bazel-focused CI Visibility support with two offline execution modes, mirroring [dd-trace-go#4503](DataDog/dd-trace-go#4503) and [dd-trace-py#17197](DataDog/dd-trace-py#17197): - **Manifest mode** (`DD_TEST_OPTIMIZATION_MANIFEST_FILE`): reads settings, known tests, flaky tests, and test management data from pre-fetched JSON cache files instead of hitting the backend. - **Payload-files mode** (`DD_TEST_OPTIMIZATION_PAYLOADS_IN_FILES`): writes CI test cycle, coverage, and tracer telemetry to `$TEST_UNDECLARED_OUTPUTS_DIR/payloads/{tests,coverage,telemetry}/*.json` instead of POSTing them. ## Key Changes - `BazelMode` (internal-api): detects both modes, resolves the manifest path via Bazel's rlocation algorithm, parses the `version=<int>` header, and exposes the `tests/`, `coverage/`, and `telemetry/` output directories. - `FileBasedConfigurationApi` (agent-ci-visibility): reads the same JSON envelopes as the HTTP API from disk; null paths return safe defaults. - `FileBasedPayloadDispatcher` (dd-trace-core): serializes CI test cycle and coverage spans as JSON files; strips `ci.*`/`git.*`/`runtime.*`/`os.*` tags to avoid cache invalidation; atomic temp-file + rename. Writes `trace_id`/`span_id`/`parent_id` as unsigned 64-bit JSON numbers (not strings) so backend schema validation passes. - `FileBasedTelemetryClient` (telemetry): subclass of `TelemetryClient` that writes the existing Moshi-encoded telemetry request body to a file; `TelemetryRouter` gets a single-client path that skips feature discovery; `TelemetrySystem` swaps in the file-based client when Bazel mode is active. - `WriterFactory` / `CiVisibilityServices` / `CiVisibilityRepoServices`: wire the file-based dispatcher/config API, disable the git client, and skip git-data upload when Bazel mode is active. - `CoreTracer`: in Bazel payload-files mode, uses `StreamingTraceCollector` (streams each CI Visibility span individually) and `DDIntakeTraceInterceptor` (not the APM-protocol interceptor, which strips `test_{session,module,suite}_end` spans) — same treatment as agentless, so all CITESTCYCLE events reach the file dispatcher. - `JUnit4TracingListener` / `JUnit4Utils`: lazy-register the test suite in `testStarted` so runners that don't fire `testSuiteStarted` still produce a proper suite span; unwrap `com.google.testing.junit.junit4.runner.RunNotifierWrapper` in `runListenersFromRunNotifier` so the idempotency check sees listeners installed on the inner notifier (fixes duplicate-listener installation under `BazelTestRunner`). - `Config`: adds `DD_TEST_OPTIMIZATION_MANIFEST_FILE` and `DD_TEST_OPTIMIZATION_PAYLOADS_IN_FILES`; skips API-key validation in these modes. `TEST_UNDECLARED_OUTPUTS_DIR` is read directly in `BazelMode` (it's a Bazel-provided env var, not a DD config). # Motivation Bazel can run tests in hermetic sandboxes with no network access. The existing CI Visibility pipeline requires HTTP calls to fetch configuration and submit payloads, which is incompatible with Bazel's execution model. Most of our operations, such as tagging tests with git metadata, also invalid Bazel's cache. This PR enables CI Visibility under Bazel by reading configuration from pre-fetched cache files and writing payloads/telemetry to files, with the orchestration of everything else being handled by our custom testing rule. # Additional Notes - Unit tests cover each new component: `BazelModeTest`, `FileBasedConfigurationApiTest` (shares the existing `*-response.ftl` fixtures with `ConfigurationApiImplTest` to keep the HTTP and file code paths in sync), `FileBasedPayloadDispatcherTest`, `FileBasedTelemetryClientTest`, and extended `TelemetryRouterSpecification`. - End-to-end repro validated locally against `DataDog/rules_test_optimization_tests`: 3 `test` + 1 `test_suite_end` + 1 `test_module_end` + 1 `test_session_end` events emitted to the payload file, no duplicate listener errors, no schema-validation failures. - Future work, not included in this PR to avoid changes too big: - Include instrumentation improvements to better handle Bazel's custom JUnit4 test runner - Refactoring of configuration API related DTOs to common utilities - Define a specification for mapping and serializing CI Vis spans to avoid logic mirroring between the two approaches (original vs file-based) - Possibly introduce a smoke test for e2e testing of bazel process, to avoid dependencies on an external repository. # Contributor Checklist - [x] Format the title according to [the contribution guidelines](https://github.com/DataDog/dd-trace-java/blob/master/CONTRIBUTING.md#title-format) - [ ] Assign the `type:` and (`comp:` or `inst:`) labels in addition to [any other useful labels](https://github.com/DataDog/dd-trace-java/blob/master/CONTRIBUTING.md#labels) - [ ] Update the [CODEOWNERS](https://github.com/DataDog/dd-trace-java/blob/master/.github/CODEOWNERS) file on source file addition, migration, or deletion - [ ] Update [public documentation](https://docs.datadoghq.com/tracing/trace_collection/library_config/java/) with any new configuration flags or behaviors Jira ticket: [SDTEST-3335] ***Note:*** **Once your PR is ready to merge, add it to the merge queue by commenting \`/merge\`.** \`/merge -c\` cancels the queue request. \`/merge -f --reason "reason"\` skips all merge queue checks; please use this judiciously, as some checks do not run at the PR-level. For more information, see [this doc](https://datadoghq.atlassian.net/wiki/spaces/DEVX/pages/3121612126/MergeQueue). [SDTEST-3335]: https://datadoghq.atlassian.net/browse/SDTEST-3335?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Co-authored-by: daniel.mohedano <daniel.mohedano@datadoghq.com>
1 parent 1ce5d90 commit 2b95dcf

24 files changed

Lines changed: 2461 additions & 134 deletions

File tree

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityRepoServices.java

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import datadog.communication.BackendApi;
44
import datadog.trace.api.Config;
5+
import datadog.trace.api.civisibility.config.BazelMode;
56
import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector;
67
import datadog.trace.api.civisibility.telemetry.tag.Provider;
78
import datadog.trace.api.git.CommitInfo;
@@ -19,6 +20,7 @@
1920
import datadog.trace.civisibility.config.ExecutionSettings;
2021
import datadog.trace.civisibility.config.ExecutionSettingsFactory;
2122
import datadog.trace.civisibility.config.ExecutionSettingsFactoryImpl;
23+
import datadog.trace.civisibility.config.FileBasedConfigurationApi;
2224
import datadog.trace.civisibility.config.JvmInfo;
2325
import datadog.trace.civisibility.config.MultiModuleExecutionSettingsFactory;
2426
import datadog.trace.civisibility.git.tree.GitClient;
@@ -81,15 +83,22 @@ public class CiVisibilityRepoServices {
8183

8284
ciTags = new CITagsProvider().getCiTags(ciInfo, pullRequestInfo);
8385

84-
gitDataUploader =
85-
buildGitDataUploader(
86-
services.config,
87-
services.metricCollector,
88-
services.gitInfoProvider,
89-
gitClient,
90-
gitRepoUnshallow,
91-
services.backendApi,
92-
repoRoot);
86+
if (BazelMode.get().isEnabled()) {
87+
// bazel rule takes care of the git data upload
88+
LOGGER.info("[bazel mode] Skipping git data upload");
89+
gitDataUploader = () -> CompletableFuture.completedFuture(null);
90+
} else {
91+
gitDataUploader =
92+
buildGitDataUploader(
93+
services.config,
94+
services.metricCollector,
95+
services.gitInfoProvider,
96+
gitClient,
97+
gitRepoUnshallow,
98+
services.backendApi,
99+
repoRoot);
100+
}
101+
93102
repoIndexProvider = services.repoIndexProviderFactory.create(repoRoot);
94103
codeowners = buildCodeowners(repoRoot);
95104
sourcePathResolver = buildSourcePathResolver(repoRoot, repoIndexProvider);
@@ -242,7 +251,17 @@ private static ExecutionSettingsFactory buildExecutionSettingsFactory(
242251
PullRequestInfo pullRequestInfo,
243252
@Nullable String repoRoot) {
244253
ConfigurationApi configurationApi;
245-
if (backendApi == null) {
254+
BazelMode bazelMode = BazelMode.get();
255+
if (bazelMode.isManifestModeEnabled()) {
256+
LOGGER.info("[bazel mode] Manifest mode detected. Using file-based configuration API");
257+
configurationApi =
258+
new FileBasedConfigurationApi(
259+
bazelMode.getSettingsPath(),
260+
null,
261+
bazelMode.getFlakyTestsPath(),
262+
bazelMode.getKnownTestsPath(),
263+
bazelMode.getTestManagementPath());
264+
} else if (backendApi == null) {
246265
LOGGER.warn(
247266
"Remote config and skippable tests requests will be skipped since backend API client could not be created");
248267
configurationApi = ConfigurationApi.NO_OP;

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityServices.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import datadog.communication.ddagent.SharedCommunicationObjects;
66
import datadog.communication.util.IOUtils;
77
import datadog.trace.api.Config;
8+
import datadog.trace.api.civisibility.config.BazelMode;
89
import datadog.trace.api.civisibility.telemetry.CiVisibilityCountMetric;
910
import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector;
1011
import datadog.trace.api.civisibility.telemetry.tag.Command;
@@ -83,7 +84,14 @@ public class CiVisibilityServices {
8384
this.backendApi = new BackendApiFactory(config, sco).createBackendApi(Intake.API);
8485
this.ciIntake = new BackendApiFactory(config, sco).createBackendApi(Intake.CI_INTAKE);
8586
this.jvmInfoFactory = new CachingJvmInfoFactory(config, new JvmInfoFactoryImpl());
86-
this.gitClientFactory = buildGitClientFactory(config, metricCollector);
87+
88+
if (BazelMode.get().isPayloadFilesEnabled()) {
89+
// git commands should not be executed in payload files mode
90+
logger.info("[bazel mode] Payload-in-files mode detected. Disabling git commands");
91+
this.gitClientFactory = r -> NoOpGitClient.INSTANCE;
92+
} else {
93+
this.gitClientFactory = buildGitClientFactory(config, metricCollector);
94+
}
8795

8896
this.environment = buildCiEnvironment();
8997
this.ciProviderInfoFactory = new CIProviderInfoFactory(config, environment);

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApiImpl.java

Lines changed: 3 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package datadog.trace.civisibility.config;
22

3-
import com.squareup.moshi.FromJson;
43
import com.squareup.moshi.Json;
54
import com.squareup.moshi.JsonAdapter;
65
import com.squareup.moshi.Moshi;
7-
import com.squareup.moshi.ToJson;
86
import com.squareup.moshi.Types;
97
import datadog.communication.BackendApi;
108
import datadog.communication.http.OkHttpUtils;
@@ -27,10 +25,8 @@
2725
import datadog.trace.api.civisibility.telemetry.tag.TestManagementEnabled;
2826
import datadog.trace.civisibility.communication.TelemetryListener;
2927
import datadog.trace.util.RandomUtils;
30-
import java.io.File;
3128
import java.io.IOException;
3229
import java.lang.reflect.ParameterizedType;
33-
import java.util.Base64;
3430
import java.util.BitSet;
3531
import java.util.Collection;
3632
import java.util.Collections;
@@ -87,7 +83,7 @@ public ConfigurationApiImpl(BackendApi backendApi, CiVisibilityMetricCollector m
8783
.add(ConfigurationsJsonAdapter.INSTANCE)
8884
.add(CiVisibilitySettings.JsonAdapter.INSTANCE)
8985
.add(EarlyFlakeDetectionSettings.JsonAdapter.INSTANCE)
90-
.add(MetaDtoJsonAdapter.INSTANCE)
86+
.add(MetaDto.JsonAdapter.INSTANCE)
9187
.build();
9288

9389
ParameterizedType requestType =
@@ -208,7 +204,7 @@ public SkippableTests getSkippableTests(TracerEnvironment tracerEnvironment) thr
208204
metricCollector.add(
209205
CiVisibilityCountMetric.ITR_SKIPPABLE_TESTS_RESPONSE_TESTS, response.data.size());
210206

211-
String correlationId = response.meta != null ? response.meta.correlation_id : null;
207+
String correlationId = response.meta != null ? response.meta.correlationId : null;
212208
Map<String, BitSet> coveredLinesByRelativeSourcePath =
213209
response.meta != null && response.meta.coverage != null
214210
? response.meta.coverage
@@ -499,6 +495,7 @@ private MultiEnvelopeDto(Collection<DataDto<T>> data, MetaDto meta) {
499495
}
500496

501497
private static final class DataDto<T> {
498+
// TODO: extract all DTO logic to common utilities
502499
private final String id;
503500
private final String type;
504501
private final T attributes;
@@ -514,52 +511,6 @@ public T getAttributes() {
514511
}
515512
}
516513

517-
private static final class MetaDto {
518-
private final String correlation_id;
519-
private final Map<String, BitSet> coverage;
520-
521-
private MetaDto(String correlation_id, Map<String, BitSet> coverage) {
522-
this.correlation_id = correlation_id;
523-
this.coverage = coverage;
524-
}
525-
}
526-
527-
private static final class MetaDtoJsonAdapter {
528-
529-
private static final MetaDtoJsonAdapter INSTANCE = new MetaDtoJsonAdapter();
530-
531-
@FromJson
532-
public MetaDto fromJson(Map<String, Object> json) {
533-
if (json == null) {
534-
return null;
535-
}
536-
537-
Map<String, BitSet> coverage;
538-
Map<String, String> encodedCoverage = (Map<String, String>) json.get("coverage");
539-
if (encodedCoverage != null) {
540-
coverage = new HashMap<>();
541-
for (Map.Entry<String, String> e : encodedCoverage.entrySet()) {
542-
String relativeSourceFilePath = e.getKey();
543-
String normalizedSourceFilePath =
544-
relativeSourceFilePath.startsWith(File.separator)
545-
? relativeSourceFilePath.substring(1)
546-
: relativeSourceFilePath;
547-
byte[] decodedLines = Base64.getDecoder().decode(e.getValue());
548-
coverage.put(normalizedSourceFilePath, BitSet.valueOf(decodedLines));
549-
}
550-
} else {
551-
coverage = null;
552-
}
553-
554-
return new MetaDto((String) json.get("correlation_id"), coverage);
555-
}
556-
557-
@ToJson
558-
public Map<String, Object> toJson(MetaDto metaDto) {
559-
throw new UnsupportedOperationException();
560-
}
561-
}
562-
563514
private static final class KnownTestsDto {
564515
private final Map<String, Map<String, List<String>>> tests;
565516

@@ -648,66 +599,4 @@ private TestManagementDto(
648599
this.branch = branch;
649600
}
650601
}
651-
652-
private static final class TestManagementTestsDto {
653-
private static final class Properties {
654-
private final Map<String, Boolean> properties;
655-
656-
private Properties(Map<String, Boolean> properties) {
657-
this.properties = properties;
658-
}
659-
660-
public Boolean isQuarantined() {
661-
return properties != null
662-
? properties.getOrDefault(TestSetting.QUARANTINED.asString(), false)
663-
: false;
664-
}
665-
666-
public Boolean isDisabled() {
667-
return properties != null
668-
? properties.getOrDefault(TestSetting.DISABLED.asString(), false)
669-
: false;
670-
}
671-
672-
public Boolean isAttemptToFix() {
673-
return properties != null
674-
? properties.getOrDefault(TestSetting.ATTEMPT_TO_FIX.asString(), false)
675-
: false;
676-
}
677-
}
678-
679-
private static final class Tests {
680-
private final Map<String, Properties> tests;
681-
682-
private Tests(Map<String, Properties> tests) {
683-
this.tests = tests;
684-
}
685-
686-
public Map<String, Properties> getTests() {
687-
return tests != null ? tests : Collections.emptyMap();
688-
}
689-
}
690-
691-
private static final class Suites {
692-
private final Map<String, Tests> suites;
693-
694-
private Suites(Map<String, Tests> suites) {
695-
this.suites = suites;
696-
}
697-
698-
public Map<String, Tests> getSuites() {
699-
return suites != null ? suites : Collections.emptyMap();
700-
}
701-
}
702-
703-
private final Map<String, Suites> modules;
704-
705-
private TestManagementTestsDto(Map<String, Suites> modules) {
706-
this.modules = modules;
707-
}
708-
709-
public Map<String, Suites> getModules() {
710-
return modules != null ? modules : Collections.emptyMap();
711-
}
712-
}
713602
}

0 commit comments

Comments
 (0)