|
40 | 40 | import java.util.Map; |
41 | 41 | import java.util.Set; |
42 | 42 | import java.util.stream.Stream; |
| 43 | +import org.junit.jupiter.api.Test; |
43 | 44 | import org.junit.jupiter.params.ParameterizedTest; |
44 | 45 | import org.junit.jupiter.params.provider.Arguments; |
45 | 46 | import org.junit.jupiter.params.provider.MethodSource; |
@@ -621,6 +622,114 @@ void testCollectTraces(String caseName, List<SpanSpec> specs) throws IOException |
621 | 622 | } |
622 | 623 | } |
623 | 624 |
|
| 625 | + @Test |
| 626 | + void testCollectMultipleTraces() throws IOException { |
| 627 | + // Three independent traces — each root span gets its own auto-generated trace ID. |
| 628 | + List<DDSpan> trace1 = |
| 629 | + buildSpans(asList(span("trace1.root", "op.root", "web"), childSpan("trace1.child", 0))); |
| 630 | + List<DDSpan> trace2 = buildSpans(asList(span("trace2.root", "op.root", "db"))); |
| 631 | + List<DDSpan> trace3 = |
| 632 | + buildSpans( |
| 633 | + asList( |
| 634 | + span("trace3.a", "op.a", "web"), |
| 635 | + span("trace3.b", "op.b", "web"), |
| 636 | + span("trace3.c", "op.c", "web"))); |
| 637 | + |
| 638 | + // Sanity: all three traces must have distinct trace IDs. |
| 639 | + DDTraceId traceId1 = trace1.get(0).getTraceId(); |
| 640 | + DDTraceId traceId2 = trace2.get(0).getTraceId(); |
| 641 | + DDTraceId traceId3 = trace3.get(0).getTraceId(); |
| 642 | + assertNotEquals(traceId1, traceId2, "trace IDs must be distinct"); |
| 643 | + assertNotEquals(traceId2, traceId3, "trace IDs must be distinct"); |
| 644 | + assertNotEquals(traceId1, traceId3, "trace IDs must be distinct"); |
| 645 | + |
| 646 | + OtlpTraceProtoCollector.INSTANCE.addTrace(trace1); |
| 647 | + OtlpTraceProtoCollector.INSTANCE.addTrace(trace2); |
| 648 | + OtlpTraceProtoCollector.INSTANCE.addTrace(trace3); |
| 649 | + OtlpPayload payload = OtlpTraceProtoCollector.INSTANCE.collectTraces(); |
| 650 | + |
| 651 | + // Collect all span IDs we expect to find across all three traces. |
| 652 | + Set<Long> expectedSpanIds = new HashSet<>(); |
| 653 | + Set<Long> expectedTraceIds = new HashSet<>(); |
| 654 | + for (DDSpan s : trace1) { |
| 655 | + expectedSpanIds.add(s.getSpanId()); |
| 656 | + expectedTraceIds.add(s.getTraceId().toLong()); |
| 657 | + } |
| 658 | + for (DDSpan s : trace2) { |
| 659 | + expectedSpanIds.add(s.getSpanId()); |
| 660 | + expectedTraceIds.add(s.getTraceId().toLong()); |
| 661 | + } |
| 662 | + for (DDSpan s : trace3) { |
| 663 | + expectedSpanIds.add(s.getSpanId()); |
| 664 | + expectedTraceIds.add(s.getTraceId().toLong()); |
| 665 | + } |
| 666 | + int totalSpans = trace1.size() + trace2.size() + trace3.size(); // 6 |
| 667 | + |
| 668 | + ByteArrayOutputStream baos = new ByteArrayOutputStream(payload.getContentLength()); |
| 669 | + payload.drain(baos::write); |
| 670 | + byte[] bytes = baos.toByteArray(); |
| 671 | + assertTrue(bytes.length > 0, "multi-trace payload must be non-empty"); |
| 672 | + |
| 673 | + // Parse TracesData → ResourceSpans → ScopeSpans → extract span_id and trace_id per span. |
| 674 | + CodedInputStream td = CodedInputStream.newInstance(bytes); |
| 675 | + int tdTag = td.readTag(); |
| 676 | + assertEquals(1, WireFormat.getTagFieldNumber(tdTag), "TracesData.resource_spans is field 1"); |
| 677 | + assertEquals(WireFormat.WIRETYPE_LENGTH_DELIMITED, WireFormat.getTagWireType(tdTag)); |
| 678 | + CodedInputStream rs = td.readBytes().newCodedInput(); |
| 679 | + assertTrue(td.isAtEnd(), "expected exactly one ResourceSpans"); |
| 680 | + |
| 681 | + CodedInputStream ss = null; |
| 682 | + while (!rs.isAtEnd()) { |
| 683 | + int rsTag = rs.readTag(); |
| 684 | + if (WireFormat.getTagFieldNumber(rsTag) == 2) { |
| 685 | + ss = rs.readBytes().newCodedInput(); |
| 686 | + } else { |
| 687 | + rs.skipField(rsTag); |
| 688 | + } |
| 689 | + } |
| 690 | + assertNotNull(ss, "ScopeSpans must be present in ResourceSpans"); |
| 691 | + |
| 692 | + Set<Long> parsedSpanIds = new HashSet<>(); |
| 693 | + Set<Long> parsedTraceIds = new HashSet<>(); |
| 694 | + while (!ss.isAtEnd()) { |
| 695 | + int ssTag = ss.readTag(); |
| 696 | + if (WireFormat.getTagFieldNumber(ssTag) == 2) { |
| 697 | + CodedInputStream sp = ss.readBytes().newCodedInput(); |
| 698 | + byte[] parsedTraceId = null; |
| 699 | + byte[] parsedSpanId = null; |
| 700 | + while (!sp.isAtEnd()) { |
| 701 | + int spTag = sp.readTag(); |
| 702 | + switch (WireFormat.getTagFieldNumber(spTag)) { |
| 703 | + case 1: |
| 704 | + parsedTraceId = sp.readBytes().toByteArray(); |
| 705 | + break; |
| 706 | + case 2: |
| 707 | + parsedSpanId = sp.readBytes().toByteArray(); |
| 708 | + break; |
| 709 | + default: |
| 710 | + sp.skipField(spTag); |
| 711 | + } |
| 712 | + } |
| 713 | + assertNotNull(parsedSpanId, "span_id must be present in every span"); |
| 714 | + assertNotNull(parsedTraceId, "trace_id must be present in every span"); |
| 715 | + assertEquals(16, parsedTraceId.length, "trace_id must be 16 bytes"); |
| 716 | + assertEquals(8, parsedSpanId.length, "span_id must be 8 bytes"); |
| 717 | + parsedSpanIds.add(readLittleEndianLong(parsedSpanId)); |
| 718 | + parsedTraceIds.add(readLittleEndianLong(parsedTraceId)); |
| 719 | + } else { |
| 720 | + ss.skipField(ssTag); |
| 721 | + } |
| 722 | + } |
| 723 | + |
| 724 | + assertEquals( |
| 725 | + totalSpans, parsedSpanIds.size(), "all spans from all traces must appear in payload"); |
| 726 | + assertEquals(expectedSpanIds, parsedSpanIds, "span IDs in payload must match those built"); |
| 727 | + assertEquals( |
| 728 | + expectedTraceIds.size(), |
| 729 | + parsedTraceIds.size(), |
| 730 | + "payload must contain spans with all three distinct trace IDs"); |
| 731 | + } |
| 732 | + |
624 | 733 | // ── span construction ───────────────────────────────────────────────────── |
625 | 734 |
|
626 | 735 | /** Builds {@link DDSpan} instances from the given specs, collecting them in order. */ |
|
0 commit comments