|
4 | 4 | package apiv3 |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "bytes" |
7 | 8 | "encoding/json" |
8 | 9 | "errors" |
9 | 10 | "fmt" |
@@ -392,7 +393,7 @@ func TestHTTPGatewayGetOperationsErrors(t *testing.T) { |
392 | 393 | assert.Contains(t, w.Body.String(), assert.AnError.Error()) |
393 | 394 | } |
394 | 395 |
|
395 | | -// TestHTTPGatewayStreamingResponse verifies that streaming produces valid JSON array |
| 396 | +// TestHTTPGatewayStreamingResponse verifies that streaming produces valid NDJSON |
396 | 397 | func TestHTTPGatewayStreamingResponse(t *testing.T) { |
397 | 398 | gw := setupHTTPGatewayNoServer(t, "") |
398 | 399 |
|
@@ -428,13 +429,21 @@ func TestHTTPGatewayStreamingResponse(t *testing.T) { |
428 | 429 |
|
429 | 430 | body := w.Body.String() |
430 | 431 |
|
431 | | - // Verify response is valid JSON array |
432 | | - var jsonArray []map[string]any |
433 | | - err = json.Unmarshal([]byte(body), &jsonArray) |
434 | | - require.NoError(t, err, "Response should be valid JSON array") |
| 432 | + // Verify response contains newline-separated JSON objects (NDJSON) |
| 433 | + lines := bytes.Split([]byte(body), []byte("\n")) |
| 434 | + nonEmptyLines := 0 |
| 435 | + for _, line := range lines { |
| 436 | + if len(bytes.TrimSpace(line)) > 0 { |
| 437 | + nonEmptyLines++ |
| 438 | + // Each line should be valid JSON |
| 439 | + var obj map[string]any |
| 440 | + err := json.Unmarshal(line, &obj) |
| 441 | + require.NoError(t, err, "Each line should be valid JSON") |
| 442 | + } |
| 443 | + } |
435 | 444 |
|
436 | | - // We should have individual trace objects in the array |
437 | | - assert.GreaterOrEqual(t, len(jsonArray), 1, "Should have at least 1 trace") |
| 445 | + // We should have multiple trace objects |
| 446 | + assert.GreaterOrEqual(t, nonEmptyLines, 1, "Should have at least 1 trace") |
438 | 447 | assert.Contains(t, body, "foobar") // First trace span name |
439 | 448 | assert.Contains(t, body, "second-span") // Second trace span name |
440 | 449 | } |
@@ -473,10 +482,18 @@ func TestHTTPGatewayStreamingMultipleBatches(t *testing.T) { |
473 | 482 |
|
474 | 483 | body := w.Body.String() |
475 | 484 |
|
476 | | - // Verify response is valid JSON |
477 | | - var jsonArray []map[string]any |
478 | | - err = json.Unmarshal([]byte(body), &jsonArray) |
479 | | - require.NoError(t, err, "Response should be valid JSON array") |
| 485 | + // Verify response contains valid NDJSON |
| 486 | + lines := bytes.Split([]byte(body), []byte("\n")) |
| 487 | + nonEmptyLines := 0 |
| 488 | + for _, line := range lines { |
| 489 | + if len(bytes.TrimSpace(line)) > 0 { |
| 490 | + nonEmptyLines++ |
| 491 | + var obj map[string]any |
| 492 | + err := json.Unmarshal(line, &obj) |
| 493 | + require.NoError(t, err, "Each line should be valid JSON") |
| 494 | + } |
| 495 | + } |
| 496 | + assert.GreaterOrEqual(t, nonEmptyLines, 1, "Should have at least 1 trace") |
480 | 497 |
|
481 | 498 | assert.Contains(t, body, "foobar") |
482 | 499 | assert.Contains(t, body, "batch2-span") |
@@ -587,10 +604,18 @@ func TestHTTPGatewayStreamingWithEmptyBatches(t *testing.T) { |
587 | 604 | assert.Equal(t, http.StatusOK, w.Code) |
588 | 605 |
|
589 | 606 | body := w.Body.String() |
590 | | - // Verify response is valid JSON |
591 | | - var jsonArray []map[string]any |
592 | | - err = json.Unmarshal([]byte(body), &jsonArray) |
593 | | - require.NoError(t, err, "Response should be valid JSON array") |
| 607 | + // Verify response contains valid NDJSON |
| 608 | + lines := bytes.Split([]byte(body), []byte("\n")) |
| 609 | + nonEmptyLines := 0 |
| 610 | + for _, line := range lines { |
| 611 | + if len(bytes.TrimSpace(line)) > 0 { |
| 612 | + nonEmptyLines++ |
| 613 | + var obj map[string]any |
| 614 | + err := json.Unmarshal(line, &obj) |
| 615 | + require.NoError(t, err, "Each line should be valid JSON") |
| 616 | + } |
| 617 | + } |
| 618 | + assert.GreaterOrEqual(t, nonEmptyLines, 1, "Should have at least 1 trace") |
594 | 619 |
|
595 | 620 | assert.Contains(t, body, "foobar") |
596 | 621 | } |
@@ -635,10 +660,10 @@ func TestHTTPGatewayFindTracesStreaming(t *testing.T) { |
635 | 660 | assert.Equal(t, http.StatusOK, w.Code) |
636 | 661 |
|
637 | 662 | body := w.Body.String() |
638 | | - // Verify response is valid JSON |
639 | | - var jsonArray []map[string]any |
640 | | - err = json.Unmarshal([]byte(body), &jsonArray) |
641 | | - require.NoError(t, err, "Response should be valid JSON array") |
| 663 | + // Verify response contains valid JSON |
| 664 | + var obj map[string]any |
| 665 | + err = json.Unmarshal([]byte(body), &obj) |
| 666 | + require.NoError(t, err, "Response should be valid JSON") |
642 | 667 |
|
643 | 668 | assert.Contains(t, body, "foobar") |
644 | 669 | } |
@@ -679,8 +704,8 @@ func TestHTTPGatewayStreamingMarshalError(t *testing.T) { |
679 | 704 | } |
680 | 705 | gw.router.ServeHTTP(w, r) |
681 | 706 |
|
682 | | - // Should log error for failing to write opening bracket (first write operation) |
683 | | - assert.Contains(t, log.String(), "Failed to write opening bracket") |
| 707 | + // Should log error for failing to marshal trace chunk (first write operation) |
| 708 | + assert.Contains(t, log.String(), "Failed to marshal trace chunk") |
684 | 709 | } |
685 | 710 |
|
686 | 711 | // failingWriter is a ResponseWriter that simulates write failures |
@@ -736,10 +761,18 @@ func TestHTTPGatewayStreamingFirstChunkWrite(t *testing.T) { |
736 | 761 | assert.Equal(t, http.StatusOK, w.Code) |
737 | 762 |
|
738 | 763 | body := w.Body.String() |
739 | | - // Verify response is valid JSON |
740 | | - var jsonArray []map[string]any |
741 | | - err = json.Unmarshal([]byte(body), &jsonArray) |
742 | | - require.NoError(t, err, "Response should be valid JSON array") |
| 764 | + // Verify response contains valid NDJSON |
| 765 | + lines := bytes.Split([]byte(body), []byte("\n")) |
| 766 | + nonEmptyLines := 0 |
| 767 | + for _, line := range lines { |
| 768 | + if len(bytes.TrimSpace(line)) > 0 { |
| 769 | + nonEmptyLines++ |
| 770 | + var obj map[string]any |
| 771 | + err := json.Unmarshal(line, &obj) |
| 772 | + require.NoError(t, err, "Each line should be valid JSON") |
| 773 | + } |
| 774 | + } |
| 775 | + assert.GreaterOrEqual(t, nonEmptyLines, 1, "Should have at least 1 trace") |
743 | 776 |
|
744 | 777 | assert.Contains(t, body, "foobar") |
745 | 778 | assert.Contains(t, body, "span2") |
@@ -817,7 +850,7 @@ func TestHTTPGatewayStreamingFallbackNoTraces(t *testing.T) { |
817 | 850 | } |
818 | 851 |
|
819 | 852 | // TestHTTPGatewayStreamingClientSideParsing verifies that the streaming response |
820 | | -// is valid JSON that clients can parse normally |
| 853 | +// is valid NDJSON that clients can parse |
821 | 854 | func TestHTTPGatewayStreamingClientSideParsing(t *testing.T) { |
822 | 855 | gw := setupHTTPGateway(t, "") |
823 | 856 |
|
@@ -872,21 +905,27 @@ func TestHTTPGatewayStreamingClientSideParsing(t *testing.T) { |
872 | 905 |
|
873 | 906 | bodyStr := string(body) |
874 | 907 |
|
875 | | - // The response MUST be valid JSON that can be parsed as a whole |
876 | | - var jsonArray []map[string]any |
877 | | - err = json.Unmarshal(body, &jsonArray) |
878 | | - require.NoError(t, err, "Response should be valid JSON array that can be parsed") |
879 | | - |
880 | | - // Verify we got multiple trace results |
881 | | - assert.GreaterOrEqual(t, len(jsonArray), 3, "Should have at least 3 trace objects in array") |
| 908 | + // The response should be NDJSON - newline-separated JSON objects |
| 909 | + lines := bytes.Split(body, []byte("\n")) |
| 910 | + validObjects := 0 |
| 911 | + for _, line := range lines { |
| 912 | + if len(bytes.TrimSpace(line)) == 0 { |
| 913 | + continue |
| 914 | + } |
| 915 | + var obj map[string]any |
| 916 | + err := json.Unmarshal(line, &obj) |
| 917 | + require.NoError(t, err, "Each line should be valid JSON") |
882 | 918 |
|
883 | | - // Each element should have a "result" field |
884 | | - for i, obj := range jsonArray { |
| 919 | + // Each object should have a "result" field |
885 | 920 | result, hasResult := obj["result"] |
886 | | - assert.True(t, hasResult, "Element %d should have a 'result' field", i) |
887 | | - assert.NotNil(t, result, "Element %d result should not be nil", i) |
| 921 | + assert.True(t, hasResult, "Object should have a 'result' field") |
| 922 | + assert.NotNil(t, result, "Object result should not be nil") |
| 923 | + validObjects++ |
888 | 924 | } |
889 | 925 |
|
| 926 | + // Verify we got multiple trace results |
| 927 | + assert.GreaterOrEqual(t, validObjects, 3, "Should have at least 3 trace objects") |
| 928 | + |
890 | 929 | // Verify all traces are present in the response |
891 | 930 | assert.Contains(t, bodyStr, "foobar", "Should contain first trace") |
892 | 931 | assert.Contains(t, bodyStr, "client-test-span", "Should contain second trace") |
@@ -968,3 +1007,41 @@ func TestHTTPGatewayStreamingEmptyTracesVsNoTraces(t *testing.T) { |
968 | 1007 | assert.Contains(t, w.Body.String(), "No traces found") |
969 | 1008 | }) |
970 | 1009 | } |
| 1010 | + |
| 1011 | +// TestHTTPGatewayStreamingSingleTraceValidJSON verifies that a single trace |
| 1012 | +// with streaming support returns valid JSON (not NDJSON) |
| 1013 | +func TestHTTPGatewayStreamingSingleTraceValidJSON(t *testing.T) { |
| 1014 | + gw := setupHTTPGatewayNoServer(t, "") |
| 1015 | + |
| 1016 | + trace1 := makeTestTrace() |
| 1017 | + gw.reader. |
| 1018 | + On("GetTraces", matchContext, mock.AnythingOfType("[]tracestore.GetTraceParams")). |
| 1019 | + Return(iter.Seq2[[]ptrace.Traces, error](func(yield func([]ptrace.Traces, error) bool) { |
| 1020 | + yield([]ptrace.Traces{trace1}, nil) |
| 1021 | + })).Once() |
| 1022 | + |
| 1023 | + r, err := http.NewRequest(http.MethodGet, "/api/v3/traces/1", http.NoBody) |
| 1024 | + require.NoError(t, err) |
| 1025 | + w := httptest.NewRecorder() |
| 1026 | + gw.router.ServeHTTP(w, r) |
| 1027 | + |
| 1028 | + assert.Equal(t, http.StatusOK, w.Code) |
| 1029 | + |
| 1030 | + body := w.Body.String() |
| 1031 | + |
| 1032 | + // Critical: Single trace should be parseable as standard JSON |
| 1033 | + var response map[string]any |
| 1034 | + err = json.Unmarshal([]byte(body), &response) |
| 1035 | + require.NoError(t, err, "Single trace response should be valid JSON") |
| 1036 | + |
| 1037 | + // Should have result field |
| 1038 | + result, hasResult := response["result"] |
| 1039 | + assert.True(t, hasResult, "Response should have 'result' field") |
| 1040 | + assert.NotNil(t, result, "Result should not be nil") |
| 1041 | + |
| 1042 | + // Should NOT contain newlines (not NDJSON) |
| 1043 | + assert.NotContains(t, body, "}\n{", "Single trace should not have NDJSON format") |
| 1044 | + |
| 1045 | + // Verify content |
| 1046 | + assert.Contains(t, body, "foobar") |
| 1047 | +} |
0 commit comments