👉 Missed Part 2? /
to decode every frame on the fly — no .proto files required — and streams the results into a browser UI. You see the JSON payloads, the status codes, how long each call took, and a ready-to-copy grpcurl command to replay any of them.
What it does
Intercepts all four gRPC stream types — unary, server-streaming, client-streaming, bidi
I wanted a browser UI that shows every gRPC call in real time. No page refresh. No polling. Just instant updates.
The challenge: One incoming gRPC request needs to go to three places at once:
Browser UI (SSE stream)
Console logs
Recorder for replay
Why SSE over WebSockets?
WebSockets are great for two-way communication. But I just needed server → browser.
SSE advantages:
Simpler protocol (just HTTP)
Auto-reconnection built in
Native EventSource API in browsers
Perfect for "fire and forget" updates
The hub pattern
The core insight: one goroutine that owns all client connections and broadcasts to them.
CODE
typeHubstruct{ clientsmap[chan]bool// Active connections broadcastchan[]byte// Incoming messages registerchanchan// New clients unregisterchanchan// Leaving clients }
func(h*Hub)Run(){ for{ select{ casech:=<-h.register: h.clients[ch]=true casech:=<-h.unregister: delete(h.clients,ch) close(ch) casemsg:=<-h.broadcast: forch:=rangeh.clients{ ch<-msg// Send to every client } } } }
How it works: Any goroutine can push to broadcast. The hub sends it to ALL connected clients. No locks. No race conditions.
Fanning out to multiple sinks
When a gRPC request comes in, I fan it out:
CODE
func(p*Proxy)handleRequest(req*Request){ // Same data to three places gop.sseHub.Broadcast(req)// Browser UI gop.logger.Log(req)// Console gop.recorder.Record(req)// For replay
// Forward to backend p.backend.Call(req) }
Each sink runs in its own goroutine. If one blocks, the others keep going.
The 40KB UI file
The frontend is a single HTML file (40KB) that:
Opens an EventSource connection to /events
Listens for new gRPC calls
Renders them as cards in real time
CODE
constsource=newEventSource('/events'); source.onmessage=(event)=>{ constcall=JSON.parse(event.data); addCallCard(call);// Render to page };
No React. No build step. Just vanilla JS that works.
What I learned
Channels as connection managers — The hub pattern feels unnatural at first, then becomes obvious
Fan-out is trivial in Go — go func() for each sink, done
SSE is underrated — For logs, metrics, UIs, it's perfect
One file is fine — My 40KB UI never needed splitting
Performance
With 100 concurrent gRPC requests:
Component Latency added
SSE broadcast ~2ms
Logger ~1ms
Recorder ~3ms
Total overhead ~6ms
All three run in parallel thanks to goroutines.
The aha! moment
Coming from Node.js, I would've used callbacks or promises. In Go, I just wrote:
SOCIAL SHARE CARD GENERATOR