Skip to content

Commit a37de9d

Browse files
committed
Made response a valid json instead of NDJSON
Signed-off-by: Somil Jain <[email protected]>
1 parent 8ae1ef8 commit a37de9d

File tree

5 files changed

+149
-113
lines changed

5 files changed

+149
-113
lines changed

cmd/query/app/apiv3/gateway_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,13 @@ func (gw *testGateway) getTracesAndVerify(t *testing.T, url string, expectedTrac
166166
require.Equal(t, http.StatusOK, statusCode, "response=%s", string(body))
167167
body = gw.verifySnapshot(t, body)
168168

169+
var jsonArray []json.RawMessage
170+
require.NoError(t, json.Unmarshal(body, &jsonArray), "response should be valid JSON array")
171+
require.NotEmpty(t, jsonArray, "response array should not be empty")
172+
169173
var response api_v3.GRPCGatewayWrapper
170-
parseResponse(t, body, &response)
174+
parseResponse(t, jsonArray[0], &response)
175+
171176
td := response.Result.ToTraces()
172177
assert.Equal(t, 1, td.SpanCount())
173178
traceID := td.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).TraceID()

cmd/query/app/apiv3/http_gateway.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,14 @@ func (h *HTTPGateway) streamTraces(tracesIter func(yield func([]ptrace.Traces, e
159159

160160
w.Header().Set("Content-Type", "application/json")
161161
w.Header().Set("X-Content-Type-Options", "nosniff")
162-
w.Header().Set("Content-Encoding", "identity")
163162

164163
tracesFound := false
165-
firstChunk := true
166164
hasError := false
165+
headerWritten := false
167166

168167
tracesIter(func(traces []ptrace.Traces, err error) bool {
169168
if err != nil {
170-
if firstChunk {
169+
if !headerWritten {
171170
h.tryHandleError(w, err, http.StatusInternalServerError)
172171
} else {
173172
h.Logger.Error("Error while streaming traces", zap.Error(err))
@@ -183,14 +182,33 @@ func (h *HTTPGateway) streamTraces(tracesIter func(yield func([]ptrace.Traces, e
183182
tracesFound = true
184183

185184
for _, td := range traces {
186-
tracesData := jptrace.TracesData(td)
185+
// Combine all resource spans into a single trace response
186+
combinedTrace := ptrace.NewTraces()
187+
resources := td.ResourceSpans()
188+
for i := 0; i < resources.Len(); i++ {
189+
resource := resources.At(i)
190+
resource.CopyTo(combinedTrace.ResourceSpans().AppendEmpty())
191+
}
192+
193+
tracesData := jptrace.TracesData(combinedTrace)
187194
response := &api_v3.GRPCGatewayWrapper{
188195
Result: &tracesData,
189196
}
190197

191-
if firstChunk {
198+
if !headerWritten {
192199
w.WriteHeader(http.StatusOK)
193-
firstChunk = false
200+
// Write opening bracket for JSON array
201+
if _, err := w.Write([]byte("[")); err != nil {
202+
h.Logger.Error("Failed to write opening bracket", zap.Error(err))
203+
return false
204+
}
205+
headerWritten = true
206+
} else {
207+
// Write comma separator between array elements
208+
if _, err := w.Write([]byte(",")); err != nil {
209+
h.Logger.Error("Failed to write comma separator", zap.Error(err))
210+
return false
211+
}
194212
}
195213

196214
marshaler := jsonpb.Marshaler{}
@@ -199,11 +217,6 @@ func (h *HTTPGateway) streamTraces(tracesIter func(yield func([]ptrace.Traces, e
199217
return false
200218
}
201219

202-
if _, err := w.Write([]byte("\n")); err != nil {
203-
h.Logger.Error("Failed to write chunk separator", zap.Error(err))
204-
return false
205-
}
206-
207220
flusher.Flush()
208221
}
209222

@@ -219,6 +232,15 @@ func (h *HTTPGateway) streamTraces(tracesIter func(yield func([]ptrace.Traces, e
219232
}
220233
resp, _ := json.Marshal(&errorResponse)
221234
http.Error(w, string(resp), http.StatusNotFound)
235+
return
236+
}
237+
238+
if headerWritten {
239+
// Write closing bracket for JSON array
240+
if _, err := w.Write([]byte("]")); err != nil {
241+
h.Logger.Error("Failed to write closing bracket", zap.Error(err))
242+
}
243+
flusher.Flush()
222244
}
223245
}
224246

cmd/query/app/apiv3/http_gateway_test.go

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ func TestHTTPGatewayGetOperationsErrors(t *testing.T) {
392392
assert.Contains(t, w.Body.String(), assert.AnError.Error())
393393
}
394394

395-
// TestHTTPGatewayStreamingResponse verifies that chunked encoding is used for streaming responses
395+
// TestHTTPGatewayStreamingResponse verifies that streaming produces valid JSON array
396396
func TestHTTPGatewayStreamingResponse(t *testing.T) {
397397
gw := setupHTTPGatewayNoServer(t, "")
398398

@@ -424,12 +424,17 @@ func TestHTTPGatewayStreamingResponse(t *testing.T) {
424424
gw.router.ServeHTTP(w, r)
425425

426426
assert.Equal(t, http.StatusOK, w.Code)
427-
428427
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
429428

430-
assert.Equal(t, "identity", w.Header().Get("Content-Encoding"))
431-
432429
body := w.Body.String()
430+
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")
435+
436+
// We should have individual trace objects in the array
437+
assert.GreaterOrEqual(t, len(jsonArray), 1, "Should have at least 1 trace")
433438
assert.Contains(t, body, "foobar") // First trace span name
434439
assert.Contains(t, body, "second-span") // Second trace span name
435440
}
@@ -467,6 +472,12 @@ func TestHTTPGatewayStreamingMultipleBatches(t *testing.T) {
467472
assert.Equal(t, http.StatusOK, w.Code)
468473

469474
body := w.Body.String()
475+
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")
480+
470481
assert.Contains(t, body, "foobar")
471482
assert.Contains(t, body, "batch2-span")
472483
}
@@ -574,7 +585,14 @@ func TestHTTPGatewayStreamingWithEmptyBatches(t *testing.T) {
574585
gw.router.ServeHTTP(w, r)
575586

576587
assert.Equal(t, http.StatusOK, w.Code)
577-
assert.Contains(t, w.Body.String(), "foobar")
588+
589+
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")
594+
595+
assert.Contains(t, body, "foobar")
578596
}
579597

580598
// TestHTTPGatewayStreamingNoTracesFound tests 404 when no traces exist
@@ -615,10 +633,17 @@ func TestHTTPGatewayFindTracesStreaming(t *testing.T) {
615633
gw.router.ServeHTTP(w, r)
616634

617635
assert.Equal(t, http.StatusOK, w.Code)
618-
assert.Contains(t, w.Body.String(), "foobar")
636+
637+
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")
642+
643+
assert.Contains(t, body, "foobar")
619644
}
620645

621-
// TestHTTPGatewayStreamingMarshalError tests handling of marshal errors during streaming
646+
// TestHTTPGatewayStreamingMarshalError tests handling of write errors during streaming
622647
func TestHTTPGatewayStreamingMarshalError(t *testing.T) {
623648
gw := setupHTTPGatewayNoServer(t, "")
624649

@@ -647,15 +672,15 @@ func TestHTTPGatewayStreamingMarshalError(t *testing.T) {
647672
gw.router = &mux.Router{}
648673
hgw.RegisterRoutes(gw.router)
649674

650-
// Use a ResponseWriter that fails immediately
675+
// Use a ResponseWriter that fails immediately on first write
651676
w := &failingWriter{
652677
ResponseRecorder: httptest.NewRecorder(),
653678
failImmediately: true,
654679
}
655680
gw.router.ServeHTTP(w, r)
656681

657-
// Should log the marshal error
658-
assert.Contains(t, log.String(), "Failed to marshal trace chunk")
682+
// Should log error for failing to write opening bracket (first write operation)
683+
assert.Contains(t, log.String(), "Failed to write opening bracket")
659684
}
660685

661686
// failingWriter is a ResponseWriter that simulates write failures
@@ -709,8 +734,15 @@ func TestHTTPGatewayStreamingFirstChunkWrite(t *testing.T) {
709734
gw.router.ServeHTTP(w, r)
710735

711736
assert.Equal(t, http.StatusOK, w.Code)
712-
assert.Contains(t, w.Body.String(), "foobar")
713-
assert.Contains(t, w.Body.String(), "span2")
737+
738+
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")
743+
744+
assert.Contains(t, body, "foobar")
745+
assert.Contains(t, body, "span2")
714746
}
715747

716748
// TestHTTPGatewayStreamingErrorBeforeFirstChunk tests error handling before streaming starts
@@ -784,8 +816,8 @@ func TestHTTPGatewayStreamingFallbackNoTraces(t *testing.T) {
784816
assert.Contains(t, w.ResponseRecorder.Body.String(), "No traces found")
785817
}
786818

787-
// TestHTTPGatewayStreamingClientSideParsing demonstrates how clients should parse
788-
// NDJSON (newline-delimited JSON) responses from the streaming API
819+
// TestHTTPGatewayStreamingClientSideParsing verifies that the streaming response
820+
// is valid JSON that clients can parse normally
789821
func TestHTTPGatewayStreamingClientSideParsing(t *testing.T) {
790822
gw := setupHTTPGateway(t, "")
791823

@@ -826,8 +858,6 @@ func TestHTTPGatewayStreamingClientSideParsing(t *testing.T) {
826858
assert.Equal(t, http.StatusOK, resp.StatusCode)
827859
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
828860

829-
assert.Equal(t, "identity", resp.Header.Get("Content-Encoding"), "Should use streaming path")
830-
831861
body := make([]byte, 0)
832862
buf := make([]byte, 1024)
833863
for {
@@ -842,47 +872,22 @@ func TestHTTPGatewayStreamingClientSideParsing(t *testing.T) {
842872

843873
bodyStr := string(body)
844874

845-
newlineCount := 0
846-
for _, b := range body {
847-
if b == '\n' {
848-
newlineCount++
849-
}
850-
}
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")
851879

852-
if newlineCount > 1 {
853-
var combinedObj map[string]any
854-
err = json.Unmarshal(body, &combinedObj)
855-
require.Error(t, err, "Combined response with multiple chunks should NOT be valid JSON - it's NDJSON format")
856-
}
880+
// Verify we got multiple trace results
881+
assert.GreaterOrEqual(t, len(jsonArray), 3, "Should have at least 3 trace objects in array")
857882

858-
lines := 0
859-
currentPos := 0
860-
validChunks := []map[string]any{}
861-
862-
for i := 0; i < len(body); i++ {
863-
if body[i] == '\n' {
864-
line := body[currentPos:i]
865-
if len(line) > 0 {
866-
// Each individual line MUST be valid JSON
867-
var jsonObj map[string]any
868-
err := json.Unmarshal(line, &jsonObj)
869-
require.NoError(t, err, "Each line should be valid JSON independently")
870-
871-
// Verify it has the expected structure with a "result" field
872-
result, hasResult := jsonObj["result"]
873-
assert.True(t, hasResult, "Each chunk should have a 'result' field")
874-
assert.NotNil(t, result, "Result should not be nil")
875-
876-
validChunks = append(validChunks, jsonObj)
877-
lines++
878-
}
879-
currentPos = i + 1
880-
}
883+
// Each element should have a "result" field
884+
for i, obj := range jsonArray {
885+
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)
881888
}
882889

883-
assert.GreaterOrEqual(t, lines, 1, "Should receive at least 1 NDJSON line")
884-
assert.GreaterOrEqual(t, len(validChunks), 1, "Should have parsed at least 1 valid chunk")
885-
890+
// Verify all traces are present in the response
886891
assert.Contains(t, bodyStr, "foobar", "Should contain first trace")
887892
assert.Contains(t, bodyStr, "client-test-span", "Should contain second trace")
888893
assert.Contains(t, bodyStr, "third-span", "Should contain third trace")
Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1-
{
2-
"result": {
3-
"resourceSpans": [
4-
{
5-
"resource": {},
6-
"scopeSpans": [
7-
{
8-
"scope": {},
9-
"spans": [
10-
{
11-
"kind": 2,
12-
"name": "foobar",
13-
"spanId": "0000000000000002",
14-
"status": {
15-
"code": 2
16-
},
17-
"traceId": "00000000000000000000000000000001"
18-
}
19-
]
20-
}
21-
]
22-
}
23-
]
1+
[
2+
{
3+
"result": {
4+
"resourceSpans": [
5+
{
6+
"resource": {},
7+
"scopeSpans": [
8+
{
9+
"scope": {},
10+
"spans": [
11+
{
12+
"kind": 2,
13+
"name": "foobar",
14+
"spanId": "0000000000000002",
15+
"status": {
16+
"code": 2
17+
},
18+
"traceId": "00000000000000000000000000000001"
19+
}
20+
]
21+
}
22+
]
23+
}
24+
]
25+
}
2426
}
25-
}
27+
]

0 commit comments

Comments
 (0)