Skip to content

Commit 5ca3cf3

Browse files
Sayan-cursoragent
andauthored
Adding more mo telemetry events (#247)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Adds new telemetry event types and wires them into HTTP and WebSocket proxy request paths, which may affect event volume and middleware behavior across all API calls and CDP connections. Main risk is incorrect enable/disable toggling or unexpected performance/metrics impact rather than data integrity or security logic. > > **Overview** > Introduces a new telemetry category **`api`** and extends the telemetry config/handlers so it is treated as a first-class, user-togglable category (including updated all-disabled semantics and API responses). > > Adds request instrumentation middleware that emits `api_call` events (request id, OpenAPI `operationId`, status, duration) and is dynamically enabled/disabled based on telemetry session state. The DevTools WebSocket proxy now publishes `cdp_connect` and `cdp_disconnect` system events (including disconnect reason, duration, and relayed message count), and `main.go` wires the new middleware and publisher hooks. > > Updates the generated OpenAPI types/spec (`oapi.go`, `openapi.yaml`) to include the new category and event schemas, and adds/updates unit tests covering middleware emission and telemetry toggle behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3156ff9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e39965c commit 5ca3cf3

10 files changed

Lines changed: 1690 additions & 336 deletions

File tree

server/cmd/api/api/middleware.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"sync/atomic"
8+
"time"
9+
10+
chiMiddleware "github.com/go-chi/chi/v5/middleware"
11+
12+
"github.com/kernel/kernel-images/server/lib/events"
13+
oapi "github.com/kernel/kernel-images/server/lib/oapi"
14+
)
15+
16+
// Per-request scratch shared between the chi-level HTTP middleware and the
17+
// strict-server middleware so the latter can stamp the matched operationId.
18+
type telemetryCtxKey struct{}
19+
20+
type telemetryRequestCtx struct {
21+
operationID string
22+
}
23+
24+
// Process-wide toggle for the api_call middleware. Flipped by
25+
// Enable/DisableTelemetryMiddleware; both middleware layers short-circuit
26+
// to passthroughs when false.
27+
var telemetryMiddlewareEnabled atomic.Bool
28+
29+
// EnableTelemetryMiddleware turns on api_call event emission.
30+
func EnableTelemetryMiddleware() { telemetryMiddlewareEnabled.Store(true) }
31+
32+
// DisableTelemetryMiddleware turns api_call event emission off.
33+
func DisableTelemetryMiddleware() { telemetryMiddlewareEnabled.Store(false) }
34+
35+
// TelemetryMiddlewareEnabled reports the current state.
36+
func TelemetryMiddlewareEnabled() bool { return telemetryMiddlewareEnabled.Load() }
37+
38+
// TelemetryHTTPMiddleware emits a BrowserApiCallEvent per documented operation,
39+
// capturing the final status and wall-clock duration.
40+
func TelemetryHTTPMiddleware(publish func(events.Event)) func(http.Handler) http.Handler {
41+
return func(next http.Handler) http.Handler {
42+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43+
if !telemetryMiddlewareEnabled.Load() {
44+
next.ServeHTTP(w, r)
45+
return
46+
}
47+
tc := &telemetryRequestCtx{}
48+
ctx := context.WithValue(r.Context(), telemetryCtxKey{}, tc)
49+
ww := chiMiddleware.NewWrapResponseWriter(w, r.ProtoMajor)
50+
start := time.Now()
51+
52+
next.ServeHTTP(ww, r.WithContext(ctx))
53+
54+
if tc.operationID == "" {
55+
return
56+
}
57+
data, _ := json.Marshal(oapi.BrowserApiCallEventData{
58+
RequestId: chiMiddleware.GetReqID(ctx),
59+
OperationId: tc.operationID,
60+
Status: ww.Status(),
61+
DurationMs: float32(time.Since(start).Microseconds()) / 1000.0,
62+
})
63+
publish(events.Event{
64+
Ts: time.Now().UnixMicro(),
65+
Type: "api_call",
66+
Category: events.Api,
67+
Source: oapi.BrowserEventSource{Kind: oapi.KernelApi},
68+
Data: data,
69+
})
70+
})
71+
}
72+
}
73+
74+
// TelemetryStrictMiddleware records the matched OpenAPI operationId onto the
75+
// per-request scratch so TelemetryHTTPMiddleware can include it in the event.
76+
func TelemetryStrictMiddleware() oapi.StrictMiddlewareFunc {
77+
return func(next oapi.StrictHandlerFunc, operationID string) oapi.StrictHandlerFunc {
78+
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) {
79+
if !telemetryMiddlewareEnabled.Load() {
80+
return next(ctx, w, r, request)
81+
}
82+
if tc, ok := ctx.Value(telemetryCtxKey{}).(*telemetryRequestCtx); ok {
83+
tc.operationID = operationID
84+
}
85+
return next(ctx, w, r, request)
86+
}
87+
}
88+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"sync"
9+
"testing"
10+
11+
chiMiddleware "github.com/go-chi/chi/v5/middleware"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/kernel/kernel-images/server/lib/events"
16+
oapi "github.com/kernel/kernel-images/server/lib/oapi"
17+
)
18+
19+
// recordingPublisher captures published events for assertion.
20+
type recordingPublisher struct {
21+
mu sync.Mutex
22+
events []events.Event
23+
}
24+
25+
func (rp *recordingPublisher) publish(ev events.Event) {
26+
rp.mu.Lock()
27+
defer rp.mu.Unlock()
28+
rp.events = append(rp.events, ev)
29+
}
30+
31+
func (rp *recordingPublisher) snapshot() []events.Event {
32+
rp.mu.Lock()
33+
defer rp.mu.Unlock()
34+
out := make([]events.Event, len(rp.events))
35+
copy(out, rp.events)
36+
return out
37+
}
38+
39+
// Mirrors the oapi-codegen strict dispatcher: middleware chain -> inner
40+
// handler -> response write.
41+
func fakeStrictHandler(operationID string, status int, mws []oapi.StrictMiddlewareFunc) http.Handler {
42+
inner := oapi.StrictHandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) {
43+
return nil, nil
44+
})
45+
for _, mw := range mws {
46+
inner = mw(inner, operationID)
47+
}
48+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
_, _ = inner(r.Context(), w, r, nil)
50+
w.WriteHeader(status)
51+
})
52+
}
53+
54+
// Flips the package-level toggle on for the test, restoring prior state
55+
// via t.Cleanup.
56+
func withTelemetryMiddlewareEnabled(t *testing.T) {
57+
t.Helper()
58+
prev := TelemetryMiddlewareEnabled()
59+
EnableTelemetryMiddleware()
60+
t.Cleanup(func() {
61+
if prev {
62+
EnableTelemetryMiddleware()
63+
} else {
64+
DisableTelemetryMiddleware()
65+
}
66+
})
67+
}
68+
69+
func TestTelemetryMiddleware_EmitsApiCallEventOnDocumentedRoute(t *testing.T) {
70+
withTelemetryMiddlewareEnabled(t)
71+
rp := &recordingPublisher{}
72+
chain := chiHandler(t, rp.publish, "ProcessExec", http.StatusOK)
73+
74+
req := httptest.NewRequest(http.MethodPost, "/process/exec", nil)
75+
rec := httptest.NewRecorder()
76+
chain.ServeHTTP(rec, req)
77+
78+
captured := rp.snapshot()
79+
require.Len(t, captured, 1)
80+
ev := captured[0]
81+
assert.Equal(t, "api_call", ev.Type)
82+
assert.Equal(t, events.Api, ev.Category)
83+
assert.Equal(t, oapi.KernelApi, ev.Source.Kind)
84+
85+
var data struct {
86+
RequestID string `json:"request_id"`
87+
OperationID string `json:"operation_id"`
88+
Status int `json:"status"`
89+
DurationMs float64 `json:"duration_ms"`
90+
}
91+
require.NoError(t, json.Unmarshal(ev.Data, &data))
92+
assert.NotEmpty(t, data.RequestID, "request_id should be set by chi RequestID middleware")
93+
assert.Equal(t, "ProcessExec", data.OperationID)
94+
assert.Equal(t, http.StatusOK, data.Status)
95+
assert.GreaterOrEqual(t, data.DurationMs, 0.0)
96+
}
97+
98+
func TestTelemetryMiddleware_CapturesNonOKStatus(t *testing.T) {
99+
withTelemetryMiddlewareEnabled(t)
100+
rp := &recordingPublisher{}
101+
chain := chiHandler(t, rp.publish, "ProcessExec", http.StatusInternalServerError)
102+
103+
req := httptest.NewRequest(http.MethodPost, "/process/exec", nil)
104+
rec := httptest.NewRecorder()
105+
chain.ServeHTTP(rec, req)
106+
107+
captured := rp.snapshot()
108+
require.Len(t, captured, 1)
109+
var data struct {
110+
Status int `json:"status"`
111+
}
112+
require.NoError(t, json.Unmarshal(captured[0].Data, &data))
113+
assert.Equal(t, http.StatusInternalServerError, data.Status)
114+
}
115+
116+
func TestTelemetryMiddleware_SkipsUndocumentedRoutes(t *testing.T) {
117+
withTelemetryMiddlewareEnabled(t)
118+
rp := &recordingPublisher{}
119+
mw := TelemetryHTTPMiddleware(rp.publish)
120+
plain := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121+
w.WriteHeader(http.StatusOK)
122+
}))
123+
124+
req := httptest.NewRequest(http.MethodGet, "/health", nil)
125+
chiMiddleware.RequestID(plain).ServeHTTP(httptest.NewRecorder(), req)
126+
127+
assert.Empty(t, rp.snapshot(), "no event should be emitted when operationId is unset")
128+
}
129+
130+
func TestTelemetryMiddleware_ShortCircuitsWhenDisabled(t *testing.T) {
131+
DisableTelemetryMiddleware()
132+
rp := &recordingPublisher{}
133+
chain := chiHandler(t, rp.publish, "ProcessExec", http.StatusOK)
134+
135+
req := httptest.NewRequest(http.MethodPost, "/process/exec", nil)
136+
rec := httptest.NewRecorder()
137+
chain.ServeHTTP(rec, req)
138+
139+
assert.Empty(t, rp.snapshot(), "disabled middleware must not emit")
140+
}
141+
142+
// Builds the same middleware stack as main.go: RequestID -> HTTP middleware ->
143+
// strict dispatch -> inner handler.
144+
func chiHandler(t *testing.T, publish func(events.Event), operationID string, status int) http.Handler {
145+
t.Helper()
146+
inner := fakeStrictHandler(operationID, status, []oapi.StrictMiddlewareFunc{TelemetryStrictMiddleware()})
147+
telemetry := TelemetryHTTPMiddleware(publish)(inner)
148+
return chiMiddleware.RequestID(telemetry)
149+
}

0 commit comments

Comments
 (0)