Skip to content

Commit 3778f0b

Browse files
authored
Merge pull request #158 from githubnext/copilot/add-logging-rpc-messages
2 parents 1542f1e + d46148b commit 3778f0b

5 files changed

Lines changed: 761 additions & 15 deletions

File tree

internal/logger/rpc_logger.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package logger
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
// RPCMessageType represents the direction of an RPC message
10+
type RPCMessageType string
11+
12+
const (
13+
// RPCMessageRequest represents an outbound request or inbound client request
14+
RPCMessageRequest RPCMessageType = "REQUEST"
15+
// RPCMessageResponse represents an inbound response from backend or outbound response to client
16+
RPCMessageResponse RPCMessageType = "RESPONSE"
17+
)
18+
19+
// RPCMessageDirection represents whether the message is inbound or outbound
20+
type RPCMessageDirection string
21+
22+
const (
23+
// RPCDirectionInbound represents messages coming into the gateway
24+
RPCDirectionInbound RPCMessageDirection = "IN"
25+
// RPCDirectionOutbound represents messages going out from the gateway
26+
RPCDirectionOutbound RPCMessageDirection = "OUT"
27+
)
28+
29+
const (
30+
// MaxPayloadPreviewLengthText is the maximum number of characters to include in text log preview (10KB)
31+
MaxPayloadPreviewLengthText = 10 * 1024 // 10KB
32+
// MaxPayloadPreviewLengthMarkdown is the maximum number of characters to include in markdown log preview
33+
MaxPayloadPreviewLengthMarkdown = 120
34+
)
35+
36+
// RPCMessageInfo contains information about an RPC message for logging
37+
type RPCMessageInfo struct {
38+
Direction RPCMessageDirection // IN or OUT
39+
MessageType RPCMessageType // REQUEST or RESPONSE
40+
ServerID string // Backend server ID or "client" for client messages
41+
Method string // RPC method name (for requests)
42+
PayloadSize int // Size of the payload in bytes
43+
Payload string // First N characters of payload (sanitized)
44+
Error string // Error message if any (for responses)
45+
}
46+
47+
// truncateAndSanitize truncates the payload to max length and sanitizes secrets
48+
func truncateAndSanitize(payload string, maxLength int) string {
49+
// First sanitize secrets
50+
sanitized := sanitizeSecrets(payload)
51+
52+
// Then truncate if needed
53+
if len(sanitized) > maxLength {
54+
return sanitized[:maxLength] + "..."
55+
}
56+
return sanitized
57+
}
58+
59+
// extractEssentialFields extracts key fields from the payload for logging
60+
func extractEssentialFields(payload []byte) map[string]interface{} {
61+
var data map[string]interface{}
62+
if err := json.Unmarshal(payload, &data); err != nil {
63+
return nil
64+
}
65+
66+
// Extract only essential fields
67+
essential := make(map[string]interface{})
68+
69+
// Common JSON-RPC fields
70+
if method, ok := data["method"].(string); ok {
71+
essential["method"] = method
72+
}
73+
if id, ok := data["id"]; ok {
74+
essential["id"] = id
75+
}
76+
if jsonrpc, ok := data["jsonrpc"].(string); ok {
77+
essential["jsonrpc"] = jsonrpc
78+
}
79+
80+
// For responses, include error info
81+
if errData, ok := data["error"]; ok {
82+
essential["error"] = errData
83+
}
84+
85+
// For requests, include params summary (but not full params)
86+
if params, ok := data["params"]; ok {
87+
if paramsMap, ok := params.(map[string]interface{}); ok {
88+
// Include param count and keys, but not values
89+
essential["params_keys"] = getMapKeys(paramsMap)
90+
}
91+
}
92+
93+
return essential
94+
}
95+
96+
// getMapKeys returns the keys of a map
97+
func getMapKeys(m map[string]interface{}) []string {
98+
keys := make([]string, 0, len(m))
99+
for k := range m {
100+
keys = append(keys, k)
101+
}
102+
return keys
103+
}
104+
105+
// formatRPCMessage formats an RPC message for logging
106+
func formatRPCMessage(info *RPCMessageInfo) string {
107+
// Short format: server→method (or server←resp) size payload
108+
var dir string
109+
if info.Direction == RPCDirectionOutbound {
110+
dir = "→"
111+
} else {
112+
dir = "←"
113+
}
114+
115+
var parts []string
116+
117+
// Server and direction
118+
if info.ServerID != "" {
119+
if info.Method != "" {
120+
parts = append(parts, fmt.Sprintf("%s%s%s", info.ServerID, dir, info.Method))
121+
} else {
122+
parts = append(parts, fmt.Sprintf("%s%sresp", info.ServerID, dir))
123+
}
124+
}
125+
126+
// Size
127+
parts = append(parts, fmt.Sprintf("%db", info.PayloadSize))
128+
129+
// Error (if present)
130+
if info.Error != "" {
131+
parts = append(parts, fmt.Sprintf("err:%s", info.Error))
132+
}
133+
134+
// Payload preview (if present)
135+
if info.Payload != "" {
136+
parts = append(parts, info.Payload)
137+
}
138+
139+
return strings.Join(parts, " ")
140+
}
141+
142+
// formatRPCMessageMarkdown formats an RPC message for markdown logging
143+
func formatRPCMessageMarkdown(info *RPCMessageInfo) string {
144+
// Concise format: **server**→method payload
145+
var dir string
146+
if info.Direction == RPCDirectionOutbound {
147+
dir = "→"
148+
} else {
149+
dir = "←"
150+
}
151+
152+
var message string
153+
154+
// Server, direction, and method/type
155+
if info.ServerID != "" {
156+
if info.Method != "" {
157+
message = fmt.Sprintf("**%s**%s`%s`", info.ServerID, dir, info.Method)
158+
} else {
159+
message = fmt.Sprintf("**%s**%sresp", info.ServerID, dir)
160+
}
161+
}
162+
163+
// Add size and payload inline
164+
if info.Payload != "" {
165+
message += fmt.Sprintf(" `%s`", info.Payload)
166+
}
167+
168+
// Error (if present)
169+
if info.Error != "" {
170+
message += fmt.Sprintf(" ⚠️`%s`", info.Error)
171+
}
172+
173+
return message
174+
}
175+
176+
// LogRPCRequest logs an RPC request message to both text and markdown logs
177+
func LogRPCRequest(direction RPCMessageDirection, serverID, method string, payload []byte) {
178+
// Create info for text log (with larger payload preview)
179+
infoText := &RPCMessageInfo{
180+
Direction: direction,
181+
MessageType: RPCMessageRequest,
182+
ServerID: serverID,
183+
Method: method,
184+
PayloadSize: len(payload),
185+
Payload: truncateAndSanitize(string(payload), MaxPayloadPreviewLengthText),
186+
}
187+
188+
// Log to text file
189+
LogDebug("rpc", "%s", formatRPCMessage(infoText))
190+
191+
// Create info for markdown log (with shorter payload preview)
192+
infoMarkdown := &RPCMessageInfo{
193+
Direction: direction,
194+
MessageType: RPCMessageRequest,
195+
ServerID: serverID,
196+
Method: method,
197+
PayloadSize: len(payload),
198+
Payload: truncateAndSanitize(string(payload), MaxPayloadPreviewLengthMarkdown),
199+
}
200+
201+
// Log to markdown file
202+
globalMarkdownMu.RLock()
203+
defer globalMarkdownMu.RUnlock()
204+
205+
if globalMarkdownLogger != nil {
206+
globalMarkdownLogger.Log(LogLevelDebug, "rpc", "%s", formatRPCMessageMarkdown(infoMarkdown))
207+
}
208+
}
209+
210+
// LogRPCResponse logs an RPC response message to both text and markdown logs
211+
func LogRPCResponse(direction RPCMessageDirection, serverID string, payload []byte, err error) {
212+
// Create info for text log (with larger payload preview)
213+
infoText := &RPCMessageInfo{
214+
Direction: direction,
215+
MessageType: RPCMessageResponse,
216+
ServerID: serverID,
217+
PayloadSize: len(payload),
218+
Payload: truncateAndSanitize(string(payload), MaxPayloadPreviewLengthText),
219+
}
220+
221+
if err != nil {
222+
infoText.Error = err.Error()
223+
}
224+
225+
// Log to text file
226+
LogDebug("rpc", "%s", formatRPCMessage(infoText))
227+
228+
// Create info for markdown log (with shorter payload preview)
229+
infoMarkdown := &RPCMessageInfo{
230+
Direction: direction,
231+
MessageType: RPCMessageResponse,
232+
ServerID: serverID,
233+
PayloadSize: len(payload),
234+
Payload: truncateAndSanitize(string(payload), MaxPayloadPreviewLengthMarkdown),
235+
}
236+
237+
if err != nil {
238+
infoMarkdown.Error = err.Error()
239+
}
240+
241+
// Log to markdown file
242+
globalMarkdownMu.RLock()
243+
defer globalMarkdownMu.RUnlock()
244+
245+
if globalMarkdownLogger != nil {
246+
globalMarkdownLogger.Log(LogLevelDebug, "rpc", "%s", formatRPCMessageMarkdown(infoMarkdown))
247+
}
248+
}
249+
250+
// LogRPCMessage logs a generic RPC message with custom info
251+
func LogRPCMessage(info *RPCMessageInfo) {
252+
// Log to text file
253+
LogDebug("rpc", "%s", formatRPCMessage(info))
254+
255+
// Log to markdown file
256+
globalMarkdownMu.RLock()
257+
defer globalMarkdownMu.RUnlock()
258+
259+
if globalMarkdownLogger != nil {
260+
globalMarkdownLogger.Log(LogLevelDebug, "rpc", "%s", formatRPCMessageMarkdown(info))
261+
}
262+
}

0 commit comments

Comments
 (0)