|
| 1 | +// ABOUTME: Parsers for the JSONL files produced by kubeproxy and mcpproxy. |
| 2 | +// ABOUTME: Tool calls themselves come from the live stream-json output, not from these. |
| 3 | + |
| 4 | +package fixture |
| 5 | + |
| 6 | +import ( |
| 7 | + "bufio" |
| 8 | + "bytes" |
| 9 | + "encoding/json" |
| 10 | + "os" |
| 11 | + "time" |
| 12 | +) |
| 13 | + |
| 14 | +// scanJSONL streams a JSONL file and feeds each line to decode. decode returns |
| 15 | +// (parsed, true) when the line should be appended to the result, or (_, false) |
| 16 | +// when it should be skipped. Returns an empty slice when the file is missing |
| 17 | +// or unreadable — callers treat that as "no log available." |
| 18 | +func scanJSONL[T any](path string, decode func([]byte) (T, bool)) []T { |
| 19 | + data, err := os.ReadFile(path) |
| 20 | + if err != nil { |
| 21 | + return nil |
| 22 | + } |
| 23 | + scanner := bufio.NewScanner(bytes.NewReader(data)) |
| 24 | + scanner.Buffer(make([]byte, jsonlScannerInitialBuf), jsonlScannerMaxBuf) |
| 25 | + var out []T |
| 26 | + for scanner.Scan() { |
| 27 | + v, ok := decode(scanner.Bytes()) |
| 28 | + if !ok { |
| 29 | + continue |
| 30 | + } |
| 31 | + out = append(out, v) |
| 32 | + } |
| 33 | + return out |
| 34 | +} |
| 35 | + |
| 36 | +// readKubectlAPILog parses a kubeproxy JSONL file into chronological API entries. |
| 37 | +func readKubectlAPILog(path string) []KubectlAPIEntry { |
| 38 | + return scanJSONL(path, decodeKubectlEntry) |
| 39 | +} |
| 40 | + |
| 41 | +func decodeKubectlEntry(line []byte) (KubectlAPIEntry, bool) { |
| 42 | + var ev struct { |
| 43 | + Type string `json:"type"` |
| 44 | + Time time.Time `json:"time"` |
| 45 | + Method string `json:"method"` |
| 46 | + Path string `json:"path"` |
| 47 | + Query string `json:"query"` |
| 48 | + Status int `json:"status"` |
| 49 | + Duration string `json:"duration"` |
| 50 | + Bytes int64 `json:"bytes"` |
| 51 | + } |
| 52 | + if err := json.Unmarshal(line, &ev); err != nil || ev.Type != "request" { |
| 53 | + return KubectlAPIEntry{}, false |
| 54 | + } |
| 55 | + return KubectlAPIEntry{ |
| 56 | + Time: ev.Time, |
| 57 | + Method: ev.Method, |
| 58 | + URL: joinURL(ev.Path, ev.Query), |
| 59 | + Status: ev.Status, |
| 60 | + Duration: ev.Duration, |
| 61 | + Bytes: ev.Bytes, |
| 62 | + }, true |
| 63 | +} |
| 64 | + |
| 65 | +// readMCPAPILog parses an mcpproxy JSONL file into chronological API entries. |
| 66 | +func readMCPAPILog(path string) []MCPAPIEntry { |
| 67 | + return scanJSONL(path, decodeMCPEntry) |
| 68 | +} |
| 69 | + |
| 70 | +func decodeMCPEntry(line []byte) (MCPAPIEntry, bool) { |
| 71 | + var ev struct { |
| 72 | + Type string `json:"type"` |
| 73 | + Time time.Time `json:"time"` |
| 74 | + Server string `json:"server"` |
| 75 | + Method string `json:"method"` |
| 76 | + Path string `json:"path"` |
| 77 | + Query string `json:"query"` |
| 78 | + Status int `json:"status"` |
| 79 | + Duration string `json:"duration"` |
| 80 | + Bytes int64 `json:"bytes"` |
| 81 | + RPCMethod string `json:"rpcMethod"` |
| 82 | + Tool string `json:"tool"` |
| 83 | + } |
| 84 | + if err := json.Unmarshal(line, &ev); err != nil || ev.Type != "request" { |
| 85 | + return MCPAPIEntry{}, false |
| 86 | + } |
| 87 | + return MCPAPIEntry{ |
| 88 | + Time: ev.Time, |
| 89 | + Server: ev.Server, |
| 90 | + Method: ev.Method, |
| 91 | + URL: joinURL(ev.Path, ev.Query), |
| 92 | + RPCMethod: ev.RPCMethod, |
| 93 | + Tool: ev.Tool, |
| 94 | + Status: ev.Status, |
| 95 | + Duration: ev.Duration, |
| 96 | + Bytes: ev.Bytes, |
| 97 | + }, true |
| 98 | +} |
| 99 | + |
| 100 | +func joinURL(path, query string) string { |
| 101 | + if query == "" { |
| 102 | + return path |
| 103 | + } |
| 104 | + return path + "?" + query |
| 105 | +} |
0 commit comments