Skip to content

Commit b4ae445

Browse files
authored
Merge pull request #162 from githubnext/copilot/update-jsonrpc-rendering
2 parents b457931 + 3e8e39b commit b4ae445

2 files changed

Lines changed: 147 additions & 16 deletions

File tree

internal/logger/rpc_logger.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const (
3030
// MaxPayloadPreviewLengthText is the maximum number of characters to include in text log preview (10KB)
3131
MaxPayloadPreviewLengthText = 10 * 1024 // 10KB
3232
// MaxPayloadPreviewLengthMarkdown is the maximum number of characters to include in markdown log preview
33-
MaxPayloadPreviewLengthMarkdown = 120
33+
MaxPayloadPreviewLengthMarkdown = 512
3434
)
3535

3636
// RPCMessageInfo contains information about an RPC message for logging
@@ -139,9 +139,42 @@ func formatRPCMessage(info *RPCMessageInfo) string {
139139
return strings.Join(parts, " ")
140140
}
141141

142+
// formatJSONWithoutFields formats JSON by removing specified fields and indenting with 2 spaces
143+
// Returns the formatted string and a boolean indicating if the JSON was valid
144+
func formatJSONWithoutFields(jsonStr string, fieldsToRemove []string) (string, bool) {
145+
var data map[string]interface{}
146+
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
147+
// If not valid JSON, return as-is with false
148+
return jsonStr, false
149+
}
150+
151+
// Remove specified fields
152+
for _, field := range fieldsToRemove {
153+
delete(data, field)
154+
}
155+
156+
// Re-marshal with 2-space indentation
157+
formatted, err := json.MarshalIndent(data, "", " ")
158+
if err != nil {
159+
return jsonStr, false
160+
}
161+
162+
// Compress JSON: remove newline after opening brace and before closing brace
163+
result := string(formatted)
164+
// Remove newline after opening brace
165+
result = strings.Replace(result, "{\n", "{ ", 1)
166+
// Remove newline before closing brace
167+
lastNewline := strings.LastIndex(result, "\n}")
168+
if lastNewline != -1 {
169+
result = result[:lastNewline] + " }"
170+
}
171+
172+
return result, true
173+
}
174+
142175
// formatRPCMessageMarkdown formats an RPC message for markdown logging
143176
func formatRPCMessageMarkdown(info *RPCMessageInfo) string {
144-
// Concise format: **server**→method payload
177+
// Concise format: **server**→method \n~~~ \n{formatted json} \n~~~
145178
var dir string
146179
if info.Direction == RPCDirectionOutbound {
147180
dir = "→"
@@ -160,9 +193,18 @@ func formatRPCMessageMarkdown(info *RPCMessageInfo) string {
160193
}
161194
}
162195

163-
// Add size and payload inline
196+
// Add formatted payload in code block
164197
if info.Payload != "" {
165-
message += fmt.Sprintf(" `%s`", info.Payload)
198+
// Remove jsonrpc and method fields, then format
199+
formatted, isValidJSON := formatJSONWithoutFields(info.Payload, []string{"jsonrpc", "method"})
200+
if isValidJSON {
201+
// Valid JSON: use code block for better readability (compact formatting)
202+
// Empty line before ~~~ per markdown convention
203+
message += fmt.Sprintf(" \n\n~~~\n%s\n~~~", formatted)
204+
} else {
205+
// Invalid JSON: use inline backticks to avoid malformed markdown
206+
message += fmt.Sprintf(" `%s`", formatted)
207+
}
166208
}
167209

168210
// Error (if present)

internal/logger/rpc_logger_test.go

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,10 @@ func TestFormatRPCMessage(t *testing.T) {
179179

180180
func TestFormatRPCMessageMarkdown(t *testing.T) {
181181
tests := []struct {
182-
name string
183-
info *RPCMessageInfo
184-
want []string // Strings that should be present in output
182+
name string
183+
info *RPCMessageInfo
184+
want []string // Strings that should be present in output
185+
notWant []string // Strings that should NOT be present in output
185186
}{
186187
{
187188
name: "outbound request",
@@ -191,9 +192,10 @@ func TestFormatRPCMessageMarkdown(t *testing.T) {
191192
ServerID: "github",
192193
Method: "tools/list",
193194
PayloadSize: 50,
194-
Payload: `{"jsonrpc":"2.0"}`,
195+
Payload: `{"jsonrpc":"2.0","method":"tools/list","params":{}}`,
195196
},
196-
want: []string{"**github**→`tools/list`", "`{\"jsonrpc\":\"2.0\"}`"},
197+
want: []string{"**github**→`tools/list`", "~~~", `"params"`, "{}"},
198+
notWant: []string{`"jsonrpc"`, `"method"`},
197199
},
198200
{
199201
name: "inbound response",
@@ -204,7 +206,8 @@ func TestFormatRPCMessageMarkdown(t *testing.T) {
204206
PayloadSize: 100,
205207
Payload: `{"result":{}}`,
206208
},
207-
want: []string{"**github**←resp", "`{\"result\":{}}`"},
209+
want: []string{"**github**←resp", "~~~", `"result"`},
210+
notWant: []string{`"jsonrpc"`, `"method"`},
208211
},
209212
{
210213
name: "response with error",
@@ -215,7 +218,21 @@ func TestFormatRPCMessageMarkdown(t *testing.T) {
215218
PayloadSize: 100,
216219
Error: "Connection timeout",
217220
},
218-
want: []string{"**github**←resp", "⚠️`Connection timeout`"},
221+
want: []string{"**github**←resp", "⚠️`Connection timeout`"},
222+
notWant: []string{},
223+
},
224+
{
225+
name: "invalid JSON payload uses inline backticks",
226+
info: &RPCMessageInfo{
227+
Direction: RPCDirectionOutbound,
228+
MessageType: RPCMessageRequest,
229+
ServerID: "github",
230+
Method: "tools/call",
231+
PayloadSize: 30,
232+
Payload: `{invalid json syntax}`,
233+
},
234+
want: []string{"**github**→`tools/call`", "`{invalid json syntax}`"},
235+
notWant: []string{"~~~"}, // Should NOT use code blocks for invalid JSON
219236
},
220237
}
221238

@@ -228,6 +245,78 @@ func TestFormatRPCMessageMarkdown(t *testing.T) {
228245
t.Errorf("Expected result to contain %q, got:\n%s", expected, result)
229246
}
230247
}
248+
249+
for _, notExpected := range tt.notWant {
250+
if strings.Contains(result, notExpected) {
251+
t.Errorf("Expected result NOT to contain %q, got:\n%s", notExpected, result)
252+
}
253+
}
254+
})
255+
}
256+
}
257+
258+
func TestFormatJSONWithoutFields(t *testing.T) {
259+
tests := []struct {
260+
name string
261+
input string
262+
fieldsToRemove []string
263+
wantContains []string
264+
wantNotContain []string
265+
wantValid bool
266+
}{
267+
{
268+
name: "remove jsonrpc and method",
269+
input: `{"jsonrpc":"2.0","method":"tools/call","params":{"arg":"value"},"id":1}`,
270+
fieldsToRemove: []string{"jsonrpc", "method"},
271+
wantContains: []string{`"params"`, `"arg"`, `"value"`, `"id"`},
272+
wantNotContain: []string{`"jsonrpc"`, `"method"`},
273+
wantValid: true,
274+
},
275+
{
276+
name: "indent with 2 spaces",
277+
input: `{"a":"b","c":{"d":"e"}}`,
278+
fieldsToRemove: []string{},
279+
wantContains: []string{" \"a\"", " \"c\"", " \"d\""},
280+
wantNotContain: []string{},
281+
wantValid: true,
282+
},
283+
{
284+
name: "invalid JSON returns as-is with false",
285+
input: `{invalid json}`,
286+
fieldsToRemove: []string{"jsonrpc"},
287+
wantContains: []string{`{invalid json}`},
288+
wantNotContain: []string{},
289+
wantValid: false,
290+
},
291+
{
292+
name: "empty object",
293+
input: `{}`,
294+
fieldsToRemove: []string{"jsonrpc"},
295+
wantContains: []string{`{}`},
296+
wantNotContain: []string{},
297+
wantValid: true,
298+
},
299+
}
300+
301+
for _, tt := range tests {
302+
t.Run(tt.name, func(t *testing.T) {
303+
result, isValid := formatJSONWithoutFields(tt.input, tt.fieldsToRemove)
304+
305+
if isValid != tt.wantValid {
306+
t.Errorf("Expected isValid=%v, got %v", tt.wantValid, isValid)
307+
}
308+
309+
for _, want := range tt.wantContains {
310+
if !strings.Contains(result, want) {
311+
t.Errorf("Expected result to contain %q, got:\n%s", want, result)
312+
}
313+
}
314+
315+
for _, notWant := range tt.wantNotContain {
316+
if strings.Contains(result, notWant) {
317+
t.Errorf("Expected result NOT to contain %q, got:\n%s", notWant, result)
318+
}
319+
}
231320
})
232321
}
233322
}
@@ -410,7 +499,7 @@ func TestLogRPCRequestPayloadTruncation(t *testing.T) {
410499
}
411500
defer CloseMarkdownLogger()
412501

413-
// Create a large payload (> 10KB for text, > 120 chars for markdown)
502+
// Create a large payload (> 10KB for text, > 512 chars for markdown)
414503
largeData := strings.Repeat("x", 12*1024) // 12KB of x's
415504
payload := []byte(`{"jsonrpc":"2.0","id":1,"method":"test","params":{"data":"` + largeData + `"}}`)
416505
LogRPCRequest(RPCDirectionOutbound, "backend", "test", payload)
@@ -438,7 +527,7 @@ func TestLogRPCRequestPayloadTruncation(t *testing.T) {
438527
t.Errorf("Text log contains more data than expected after truncation (should be ~10KB)")
439528
}
440529

441-
// Check markdown log - should be truncated at 120 chars
530+
// Check markdown log - should be truncated at 512 chars
442531
mdLog := filepath.Join(logDir, "test.md")
443532
mdContent, err := os.ReadFile(mdLog)
444533
if err != nil {
@@ -450,9 +539,9 @@ func TestLogRPCRequestPayloadTruncation(t *testing.T) {
450539
t.Errorf("Markdown log does not show truncation marker")
451540
}
452541

453-
// Markdown should have much less data (truncated at 120 chars)
454-
xCountMd := strings.Count(mdStr, strings.Repeat("x", 150))
542+
// Markdown should have much less data (truncated at 512 chars)
543+
xCountMd := strings.Count(mdStr, strings.Repeat("x", 600))
455544
if xCountMd > 0 {
456-
t.Errorf("Markdown log contains more data than expected after truncation (should be ~120 chars)")
545+
t.Errorf("Markdown log contains more data than expected after truncation (should be ~512 chars)")
457546
}
458547
}

0 commit comments

Comments
 (0)