diff --git a/build-info-api/src/main/java/org/jfrog/build/api/Build.java b/build-info-api/src/main/java/org/jfrog/build/api/Build.java index 5b212b805..5203f002c 100644 --- a/build-info-api/src/main/java/org/jfrog/build/api/Build.java +++ b/build-info-api/src/main/java/org/jfrog/build/api/Build.java @@ -50,6 +50,8 @@ public class Build extends BaseBuildBean { private Issues issues; + private List traces; + /** * Formats the timestamp to the ISO date time string format expected by the build info API. * @@ -466,6 +468,13 @@ public void setIssues(Issues issues) { this.issues = issues; } + public List getTraces() { + return traces; + } + + public void setTraces(List traces) { + this.traces = traces; + } @Override public String toString() { @@ -491,6 +500,7 @@ public String toString() { ", statuses=" + statuses + ", buildDependencies=" + buildDependencies + ", issues=" + issues + + ", traces=" + traces + '}'; } } \ No newline at end of file diff --git a/build-info-api/src/main/java/org/jfrog/build/api/Trace.java b/build-info-api/src/main/java/org/jfrog/build/api/Trace.java new file mode 100644 index 000000000..f2c1cd5bf --- /dev/null +++ b/build-info-api/src/main/java/org/jfrog/build/api/Trace.java @@ -0,0 +1,105 @@ +package org.jfrog.build.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.Map; + +/** + * Represents a single OpenTelemetry-style span attached to a build. + * The traces array captures CI pipeline observability data — test runs, + * commands, and their timing relationships. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Trace implements Serializable { + + @JsonProperty("trace_id") + private String traceId; + + @JsonProperty("span_id") + private String spanId; + + @JsonProperty("parent_span_id") + private String parentSpanId; + + private String name; + + @JsonProperty("start_time") + private String startTime; + + @JsonProperty("end_time") + private String endTime; + + private Map attributes; + + @JsonProperty("span_kind") + private String spanKind; + + public Trace() { + } + + public String getTraceId() { + return traceId; + } + + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + public String getSpanId() { + return spanId; + } + + public void setSpanId(String spanId) { + this.spanId = spanId; + } + + public String getParentSpanId() { + return parentSpanId; + } + + public void setParentSpanId(String parentSpanId) { + this.parentSpanId = parentSpanId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public String getSpanKind() { + return spanKind; + } + + public void setSpanKind(String spanKind) { + this.spanKind = spanKind; + } +} diff --git a/build-info-api/src/main/java/org/jfrog/build/api/builder/BuildInfoBuilder.java b/build-info-api/src/main/java/org/jfrog/build/api/builder/BuildInfoBuilder.java index 76567469f..cb50662a4 100644 --- a/build-info-api/src/main/java/org/jfrog/build/api/builder/BuildInfoBuilder.java +++ b/build-info-api/src/main/java/org/jfrog/build/api/builder/BuildInfoBuilder.java @@ -7,6 +7,7 @@ import org.jfrog.build.api.BuildRetention; import org.jfrog.build.api.Issues; import org.jfrog.build.api.MatrixParameter; +import org.jfrog.build.api.Trace; import org.jfrog.build.api.Module; import org.jfrog.build.api.Vcs; import org.jfrog.build.api.release.PromotionStatus; @@ -49,6 +50,7 @@ public class BuildInfoBuilder { protected Properties properties; protected BuildRetention buildRetention; protected Issues issues; + protected List traces; public BuildInfoBuilder(String name) { this.name = name; @@ -95,6 +97,7 @@ public Build build() { build.setVcs(vcs); build.setBuildRetention(buildRetention); build.setIssues(issues); + build.setTraces(traces); return build; } @@ -418,6 +421,11 @@ public BuildInfoBuilder issues(Issues issues) { return this; } + public BuildInfoBuilder traces(List traces) { + this.traces = traces; + return this; + } + public BuildInfoBuilder project(String project) { this.project = project; return this; diff --git a/build-info-api/src/test/java/org/jfrog/build/api/TraceTest.java b/build-info-api/src/test/java/org/jfrog/build/api/TraceTest.java new file mode 100644 index 000000000..0c8ca6740 --- /dev/null +++ b/build-info-api/src/test/java/org/jfrog/build/api/TraceTest.java @@ -0,0 +1,144 @@ +package org.jfrog.build.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; + +/** + * Tests the Trace model and its serialization / deserialization via the Build class. + */ +@Test +public class TraceTest { + + private static final String TRACE_ID = "43fb38773c526b515370ba5406ce4b89"; + + public void testTraceGettersSetters() { + Trace trace = new Trace(); + assertNull(trace.getTraceId(), "trace_id should be null initially"); + assertNull(trace.getSpanId()); + assertNull(trace.getParentSpanId()); + assertNull(trace.getName()); + assertNull(trace.getStartTime()); + assertNull(trace.getEndTime()); + assertNull(trace.getAttributes()); + assertNull(trace.getSpanKind()); + + Map attrs = new HashMap<>(); + attrs.put("span.type", "test_case"); + attrs.put("test.outcome", "pass"); + + trace.setTraceId(TRACE_ID); + trace.setSpanId("0246df031f708709"); + trace.setParentSpanId("8024dcd3910a35e6"); + trace.setName("boost ci-setup github"); + trace.setStartTime("2026-06-22T14:37:56.885735333Z"); + trace.setEndTime("2026-06-22T14:37:56.894949188Z"); + trace.setAttributes(attrs); + trace.setSpanKind("internal"); + + assertEquals(trace.getTraceId(), TRACE_ID); + assertEquals(trace.getSpanId(), "0246df031f708709"); + assertEquals(trace.getParentSpanId(), "8024dcd3910a35e6"); + assertEquals(trace.getName(), "boost ci-setup github"); + assertEquals(trace.getStartTime(), "2026-06-22T14:37:56.885735333Z"); + assertEquals(trace.getEndTime(), "2026-06-22T14:37:56.894949188Z"); + assertEquals(trace.getAttributes(), attrs); + assertEquals(trace.getSpanKind(), "internal"); + } + + public void testBuildTracesGetterSetter() { + Build build = new Build(); + assertNull(build.getTraces(), "traces should be null by default"); + + Trace t1 = buildTrace("span1", "parent1", "test run: go"); + Trace t2 = buildTrace("span2", "parent1", "testenv/TestAdd"); + List traces = Arrays.asList(t1, t2); + + build.setTraces(traces); + + assertNotNull(build.getTraces()); + assertEquals(build.getTraces().size(), 2); + assertEquals(build.getTraces().get(0).getSpanId(), "span1"); + assertEquals(build.getTraces().get(1).getName(), "testenv/TestAdd"); + } + + public void testTraceJsonRoundTrip() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + Map attrs = new HashMap<>(); + attrs.put("span.type", "test_run"); + attrs.put("test.case_count", 2); + attrs.put("test.pass_count", 2); + attrs.put("test.exit_code", 0); + + Trace trace = new Trace(); + trace.setTraceId(TRACE_ID); + trace.setSpanId("b009c2f52bd10e4a"); + trace.setParentSpanId("679767eacc094f15"); + trace.setName("test run: go"); + trace.setStartTime("2026-06-22T14:37:56.955856491Z"); + trace.setEndTime("2026-06-22T14:38:03.242776111Z"); + trace.setAttributes(attrs); + trace.setSpanKind("internal"); + + String json = mapper.writeValueAsString(trace); + + // Verify snake_case JSON property names + assert json.contains("\"trace_id\"") : "JSON should use trace_id (snake_case)"; + assert json.contains("\"span_id\"") : "JSON should use span_id"; + assert json.contains("\"parent_span_id\"") : "JSON should use parent_span_id"; + assert json.contains("\"start_time\"") : "JSON should use start_time"; + assert json.contains("\"end_time\"") : "JSON should use end_time"; + assert json.contains("\"span_kind\"") : "JSON should use span_kind"; + + Trace deserialized = mapper.readValue(json, Trace.class); + assertEquals(deserialized.getTraceId(), TRACE_ID); + assertEquals(deserialized.getSpanId(), "b009c2f52bd10e4a"); + assertEquals(deserialized.getName(), "test run: go"); + assertEquals(deserialized.getAttributes().get("test.case_count"), 2); + } + + public void testBuildWithTracesJsonRoundTrip() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + Trace t1 = buildTrace("span1", "parent0", "step 1"); + Trace t2 = buildTrace("span2", "span1", "step 2"); + + Build build = new Build(); + build.setName("my-build"); + build.setNumber("42"); + build.setStarted("2026-06-22T14:37:56.000+0000"); + build.setTraces(Arrays.asList(t1, t2)); + + String json = mapper.writeValueAsString(build); + assert json.contains("\"traces\"") : "Build JSON should include traces field"; + + Build deserialized = mapper.readValue(json, Build.class); + assertNotNull(deserialized.getTraces()); + assertEquals(deserialized.getTraces().size(), 2); + assertEquals(deserialized.getTraces().get(0).getSpanId(), "span1"); + assertEquals(deserialized.getTraces().get(1).getSpanId(), "span2"); + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private Trace buildTrace(String spanId, String parentSpanId, String name) { + Trace t = new Trace(); + t.setTraceId(TRACE_ID); + t.setSpanId(spanId); + t.setParentSpanId(parentSpanId); + t.setName(name); + t.setStartTime("2026-06-22T14:37:56.000Z"); + t.setEndTime("2026-06-22T14:37:57.000Z"); + t.setSpanKind("internal"); + return t; + } +} diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/builder/BuildInfoBuilder.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/builder/BuildInfoBuilder.java index f0b5faec7..66395eb09 100644 --- a/build-info-extractor/src/main/java/org/jfrog/build/extractor/builder/BuildInfoBuilder.java +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/builder/BuildInfoBuilder.java @@ -40,6 +40,7 @@ public class BuildInfoBuilder { protected Properties properties; protected BuildRetention buildRetention; protected Issues issues; + protected List traces; public BuildInfoBuilder(String name) { this.name = name; @@ -86,6 +87,7 @@ public BuildInfo build() { buildInfo.setVcs(vcs); buildInfo.setBuildRetention(buildRetention); buildInfo.setIssues(issues); + buildInfo.setTraces(traces); return buildInfo; } @@ -421,6 +423,11 @@ public BuildInfoBuilder issues(Issues issues) { return this; } + public BuildInfoBuilder traces(List traces) { + this.traces = traces; + return this; + } + public BuildInfoBuilder setProject(String project) { this.project = project; return this; diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfo.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfo.java index d47d710ff..8f52b934f 100644 --- a/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfo.java +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfo.java @@ -58,6 +58,8 @@ public class BuildInfo extends BaseBuildBean { private Issues issues; + private List traces; + /** * Formats the timestamp to the ISO date time string format expected by the build info API. * @@ -486,6 +488,14 @@ public void setIssues(Issues issues) { this.issues = issues; } + public List getTraces() { + return traces; + } + + public void setTraces(List traces) { + this.traces = traces; + } + public void append(BuildInfo other) { if (buildAgent == null) { setBuildAgent(other.buildAgent); @@ -574,6 +584,7 @@ public String toString() { ", statuses=" + statuses + ", buildDependencies=" + buildDependencies + ", issues=" + issues + + ", traces=" + traces + '}'; } @@ -597,7 +608,8 @@ public Build ToBuild() { .properties(getProperties()) .vcs(vcs == null ? null : vcs.stream().map(Vcs::ToBuildVcs).collect(Collectors.toList())) .buildRetention(buildRetention == null ? null : buildRetention.ToBuildRetention()) - .issues(issues == null ? null : issues.ToBuildIssues()); + .issues(issues == null ? null : issues.ToBuildIssues()) + .traces(traces); if (modules != null) { builder.modules(modules.stream().map(m -> new org.jfrog.build.api.builder.ModuleBuilder() .type(m.getType() == null ? null : ModuleType.valueOf(m.getType().toUpperCase())) @@ -634,7 +646,8 @@ public static BuildInfo ToBuildInfo(org.jfrog.build.api.Build build) { .properties(build.getProperties()) .vcs(build.getVcs() == null ? null : build.getVcs().stream().map(Vcs::ToBuildInfoVcs).collect(Collectors.toList())) .buildRetention(build.getBuildRetention() == null ? null : BuildRetention.ToBuildInfoRetention(build.getBuildRetention())) - .issues(build.getIssues() == null ? null : Issues.ToBuildInfoIssues(build.getIssues())); + .issues(build.getIssues() == null ? null : Issues.ToBuildInfoIssues(build.getIssues())) + .traces(build.getTraces()); if (build.getModules() != null) { builder.modules(build.getModules().stream().map(m -> new ModuleBuilder() .type(m.getType())