diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index b9b0a346..d06d3b3f 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -30,6 +30,16 @@ type cdpMonitorController interface { var _ cdpMonitorController = (*cdpmonitor.Monitor)(nil) +// OTLPExporter controls the optional OTLP export sink. Nil when no export +// endpoint is provisioned on the VM. Implemented by *events.OTLPExportController. +type OTLPExporter interface { + Start(ctx context.Context) error + Stop(ctx context.Context) error + Running() bool +} + +var _ OTLPExporter = (*events.OTLPExportController)(nil) + type ApiService struct { // defaultRecorderID is used whenever the caller doesn't specify an explicit ID. defaultRecorderID string @@ -89,9 +99,14 @@ type ApiService struct { eventStream *events.EventStream telemetrySession *telemetry.TelemetrySession cdpMonitor cdpMonitorController + otlpExport OTLPExporter monitorMu sync.Mutex - lifecycleCtx context.Context - lifecycleCancel context.CancelFunc + // exportMu serializes OTLP export reconciliation independently of monitorMu, + // so a toggle-off drain (bounded by otlpStopTimeout) never blocks concurrent + // telemetry reads/writes on monitorMu. + exportMu sync.Mutex + lifecycleCtx context.Context + lifecycleCancel context.CancelFunc } var _ oapi.StrictServerInterface = (*ApiService)(nil) @@ -105,6 +120,7 @@ func New( telemetrySession *telemetry.TelemetrySession, eventStream *events.EventStream, displayNum int, + otlpExport OTLPExporter, ) (*ApiService, error) { switch { case recordManager == nil: @@ -138,6 +154,7 @@ func New( eventStream: eventStream, telemetrySession: telemetrySession, cdpMonitor: mon, + otlpExport: otlpExport, lifecycleCtx: ctx, lifecycleCancel: cancel, }, nil @@ -369,5 +386,7 @@ func (s *ApiService) Shutdown(ctx context.Context) error { s.cdpMonitor.Stop() s.telemetrySession.Stop() s.monitorMu.Unlock() + // The OTLP export sink is stopped by main after the servers drain, so any + // events they emit on the way down are still exported (mirrors s2Writer). return s.recordManager.StopAll(ctx) } diff --git a/server/cmd/api/api/api_test.go b/server/cmd/api/api/api_test.go index 4f2c2165..42c00717 100644 --- a/server/cmd/api/api/api_test.go +++ b/server/cmd/api/api/api_test.go @@ -321,7 +321,7 @@ func newTelemetrySession(t *testing.T) (*telemetry.TelemetrySession, *events.Eve func newSvc(t *testing.T, mgr recorder.RecordManager) (*ApiService, error) { t.Helper() ts, es := newTelemetrySession(t) - return New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0) + return New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil) } func TestApiService_PatchChromiumFlags(t *testing.T) { diff --git a/server/cmd/api/api/display_test.go b/server/cmd/api/api/display_test.go index 154b6359..c37700c3 100644 --- a/server/cmd/api/api/display_test.go +++ b/server/cmd/api/api/display_test.go @@ -36,7 +36,7 @@ func testFFmpegFactory(t *testing.T, tempDir string) recorder.FFmpegRecorderFact func newTestServiceWithFactory(t *testing.T, mgr recorder.RecordManager, factory recorder.FFmpegRecorderFactory) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0) + svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil) require.NoError(t, err) return svc } diff --git a/server/cmd/api/api/telemetry.go b/server/cmd/api/api/telemetry.go index 4ea50992..52dc6b1f 100644 --- a/server/cmd/api/api/telemetry.go +++ b/server/cmd/api/api/telemetry.go @@ -2,6 +2,7 @@ package api import ( "context" + "time" oapi "github.com/kernel/kernel-images/server/lib/oapi" "github.com/nrednav/cuid2" @@ -12,6 +13,9 @@ import ( "github.com/kernel/kernel-images/server/lib/telemetry" ) +// otlpStopTimeout bounds how long a runtime export toggle-off waits to drain. +const otlpStopTimeout = 5 * time.Second + // GetTelemetry handles GET /telemetry. // Returns the current telemetry configuration. Returns 404 if telemetry is not configured. func (s *ApiService) GetTelemetry(_ context.Context, _ oapi.GetTelemetryRequestObject) (oapi.GetTelemetryResponseObject, error) { @@ -28,6 +32,11 @@ func (s *ApiService) GetTelemetry(_ context.Context, _ oapi.GetTelemetryRequestO // Sets the telemetry configuration. Returns 201 if not previously configured, 200 if it was. // Setting every configurable category to enabled:false clears the configuration (200). func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequestObject) (oapi.PutTelemetryResponseObject, error) { + // Reconcile export after monitorMu is released (defers run LIFO), so the + // toggle-off drain does not hold the API-wide lock. It reads the committed + // desired state from the session, so it is correct regardless of how this + // request exits. + defer s.reconcileExport(ctx) s.monitorMu.Lock() defer s.monitorMu.Unlock() @@ -73,6 +82,8 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ // Partially updates the telemetry configuration. Returns 404 if not configured. // Setting every configurable category to enabled:false clears the configuration (200). func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetryRequestObject) (oapi.PatchTelemetryResponseObject, error) { + // See PutTelemetry: reconcile export after monitorMu is released. + defer s.reconcileExport(ctx) s.monitorMu.Lock() defer s.monitorMu.Unlock() @@ -80,12 +91,14 @@ func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetry return oapi.PatchTelemetry404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "telemetry is not configured"}}, nil } - if req.Body == nil || req.Body.Browser == nil { + // Nothing to merge when neither the category block nor the export toggle is + // present; skip the reconcile and echo current state, as before. + if req.Body == nil || (req.Body.Browser == nil && req.Body.Export == nil) { return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } prev := s.telemetrySession.Config() - cfg, allDisabled := mergeTelemetryConfig(prev, req.Body.Browser) + cfg, allDisabled := mergeTelemetryConfig(prev, req.Body) if allDisabled { s.telemetrySession.Stop() s.stopTelemetryState() @@ -139,8 +152,42 @@ func (s *ApiService) rollbackTelemetry(wasActive bool, prev telemetry.TelemetryC _ = s.reconcileTelemetryState(prev.Categories) } +// reconcileExport starts or stops the OTLP export sink to match the committed +// telemetry config. It reads the desired state from the session rather than a +// parameter, and holds exportMu (not monitorMu) so the toggle-off drain does +// not block concurrent telemetry reads/writes; whichever call wins exportMu +// last observes the final committed state, so the sink converges. Callers run +// it after releasing monitorMu (via defer). Export is best-effort: a failed +// start is logged, never surfaced. No-op when no export destination is +// configured. +func (s *ApiService) reconcileExport(ctx context.Context) { + s.exportMu.Lock() + defer s.exportMu.Unlock() + + enabled := s.telemetrySession.Config().ExportOTLP + if s.otlpExport == nil { + if enabled { + logger.FromContext(ctx).Warn("otlp export enabled but no destination configured; export inactive") + } + return + } + switch { + case enabled && !s.otlpExport.Running(): + // Root the export on the app lifecycle, not this request; only its logs + // carry the request context. + if err := s.otlpExport.Start(s.lifecycleCtx); err != nil { + logger.FromContext(ctx).Error("otlp export failed to start", "err", err) + } + case !enabled && s.otlpExport.Running(): + // Draining must outlive the request, so bound it independently. + stopCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), otlpStopTimeout) + defer cancel() + _ = s.otlpExport.Stop(stopCtx) + } +} + // stopTelemetryState tears down the collector and middleware after a session is -// cleared. +// cleared. Export is reconciled separately, after monitorMu is released. func (s *ApiService) stopTelemetryState() { if s.cdpMonitor.IsRunning() { s.cdpMonitor.Stop() @@ -203,11 +250,12 @@ func containsCategory(cats []oapi.TelemetryEventCategory, target oapi.TelemetryE // the categories explicitly enabled there are captured (anything omitted is off). Returns the // config, whether the result is empty (stop signal), and any error. func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.TelemetryConfig, bool, error) { + exportOTLP := exportOTLPFromOAPI(cfg) if cfg == nil || cfg.Browser == nil { // No per-category settings: resolve to the explicit default set so the // effective categories are known before the collector is reconciled. cats := append([]oapi.TelemetryEventCategory(nil), events.DefaultCategories...) - return telemetry.TelemetryConfig{Categories: cats}, false, nil + return telemetry.TelemetryConfig{Categories: cats, ExportOTLP: exportOTLP}, false, nil } cats := make([]oapi.TelemetryEventCategory, 0, len(events.UserCategories)) @@ -219,13 +267,22 @@ func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.Teleme if len(cats) == 0 { return telemetry.TelemetryConfig{}, true, nil } - return telemetry.TelemetryConfig{Categories: cats}, false, nil + return telemetry.TelemetryConfig{Categories: cats, ExportOTLP: exportOTLP}, false, nil +} + +// exportOTLPFromOAPI reads the OTLP export toggle from a config, defaulting to +// false (off) when the export block is omitted. +func exportOTLPFromOAPI(cfg *oapi.BrowserTelemetryConfig) bool { + if cfg != nil && cfg.Export != nil && cfg.Export.Otlp != nil && cfg.Export.Otlp.Enabled != nil { + return *cfg.Export.Otlp.Enabled + } + return false } // mergeTelemetryConfig applies patch overrides onto current, returning the merged config and // whether every configurable category ended up disabled (stop signal). Only categories with an // explicit Enabled field in patch are changed; omitted categories keep their current state. -func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.BrowserTelemetryCategoriesConfig) (telemetry.TelemetryConfig, bool) { +func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.BrowserTelemetryConfig) (telemetry.TelemetryConfig, bool) { userCat := categorySetOf(events.UserCategories) active := make(map[oapi.TelemetryEventCategory]struct{}, len(current.Categories)) for _, c := range current.Categories { @@ -234,17 +291,25 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser } } - for _, f := range categoryFields(patch) { - if f.config == nil || f.config.Enabled == nil { - continue // not mentioned in patch — keep current state - } - if *f.config.Enabled { - active[f.category] = struct{}{} - } else { - delete(active, f.category) + if patch.Browser != nil { + for _, f := range categoryFields(patch.Browser) { + if f.config == nil || f.config.Enabled == nil { + continue // not mentioned in patch; keep current state + } + if *f.config.Enabled { + active[f.category] = struct{}{} + } else { + delete(active, f.category) + } } } + // Export follows the same patch semantics: an omitted toggle is unchanged. + exportOTLP := current.ExportOTLP + if patch.Export != nil && patch.Export.Otlp != nil && patch.Export.Otlp.Enabled != nil { + exportOTLP = *patch.Export.Otlp.Enabled + } + if len(active) == 0 { return telemetry.TelemetryConfig{}, true } @@ -252,7 +317,7 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser for c := range active { cats = append(cats, c) } - return telemetry.TelemetryConfig{Categories: cats}, false + return telemetry.TelemetryConfig{Categories: cats, ExportOTLP: exportOTLP}, false } // disabledConfig returns a BrowserTelemetryConfig with every configurable category explicitly disabled. @@ -272,6 +337,14 @@ func disabledConfig() oapi.BrowserTelemetryConfig { Screenshot: off(), Captcha: off(), }, + Export: exportConfigToOAPI(false), + } +} + +// exportConfigToOAPI renders the OTLP export toggle for API responses. +func exportConfigToOAPI(enabled bool) *oapi.BrowserTelemetryExportConfig { + return &oapi.BrowserTelemetryExportConfig{ + Otlp: &oapi.BrowserTelemetryOTLPExportConfig{Enabled: lo.ToPtr(enabled)}, } } @@ -295,5 +368,6 @@ func telemetryConfigToOAPI(cfg telemetry.TelemetryConfig) oapi.BrowserTelemetryC Screenshot: enabled(events.Screenshot), Captcha: enabled(events.Captcha), }, + Export: exportConfigToOAPI(cfg.ExportOTLP), } } diff --git a/server/cmd/api/api/telemetry_test.go b/server/cmd/api/api/telemetry_test.go index 258b6987..5b7261cf 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "errors" + "sync" "testing" + "time" "github.com/kernel/kernel-images/server/lib/events" oapi "github.com/kernel/kernel-images/server/lib/oapi" @@ -358,7 +360,7 @@ func (m *mockRecordManager) StopAll(_ context.Context) error func newTestService(t *testing.T, mgr recorder.RecordManager) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil) require.NoError(t, err) svc.cdpMonitor = &stubCdpMonitor{} return svc @@ -427,3 +429,156 @@ func TestTelemetryCollectorFailureLeavesConfigUnchanged(t *testing.T) { assert.ElementsMatch(t, before, svc.telemetrySession.Config().Categories, "failed PATCH must not change the persisted config") }) } + +// stubOTLPExporter records start/stop calls for export-toggle tests. +type stubOTLPExporter struct { + running bool + starts int + stops int + startErr error +} + +func (s *stubOTLPExporter) Start(context.Context) error { + if s.startErr != nil { + return s.startErr + } + s.starts++ + s.running = true + return nil +} + +func (s *stubOTLPExporter) Stop(context.Context) error { + s.stops++ + s.running = false + return nil +} + +func (s *stubOTLPExporter) Running() bool { return s.running } + +func TestTelemetryExportToggle(t *testing.T) { + ctx := context.Background() + tr, fa := true, false + exportOn := func() *oapi.BrowserTelemetryConfig { + return &oapi.BrowserTelemetryConfig{ + Browser: &oapi.BrowserTelemetryCategoriesConfig{Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}}, + Export: &oapi.BrowserTelemetryExportConfig{Otlp: &oapi.BrowserTelemetryOTLPExportConfig{Enabled: &tr}}, + } + } + + t.Run("PUT enables export, PATCH disables it", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + exp := &stubOTLPExporter{} + svc.otlpExport = exp + + resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: exportOn()}) + require.NoError(t, err) + r := resp.(oapi.PutTelemetry201JSONResponse) + assert.True(t, exp.Running(), "export should be running after enable") + assert.True(t, *r.Config.Export.Otlp.Enabled, "response should report export enabled") + + presp, err := svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ + Body: &oapi.BrowserTelemetryConfig{Export: &oapi.BrowserTelemetryExportConfig{Otlp: &oapi.BrowserTelemetryOTLPExportConfig{Enabled: &fa}}}, + }) + require.NoError(t, err) + p := presp.(oapi.PatchTelemetry200JSONResponse) + assert.False(t, exp.Running(), "export should stop after disable") + assert.False(t, *p.Config.Export.Otlp.Enabled) + }) + + t.Run("export defaults off when unspecified", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + exp := &stubOTLPExporter{} + svc.otlpExport = exp + + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) + require.NoError(t, err) + assert.False(t, exp.Running(), "export must stay off without an explicit toggle") + assert.Equal(t, 0, exp.starts) + }) + + t.Run("enabling export with no endpoint provisioned is a no-op", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) // otlpExport left nil + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: exportOn()}) + require.NoError(t, err, "a missing endpoint must not fail the telemetry apply") + }) + + t.Run("clearing the session stops export", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + exp := &stubOTLPExporter{} + svc.otlpExport = exp + + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: exportOn()}) + require.NoError(t, err) + require.True(t, exp.Running()) + + _, err = svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: &oapi.BrowserTelemetryConfig{Browser: allCategoriesDisabled()}}) + require.NoError(t, err) + assert.False(t, exp.Running(), "clearing telemetry should stop export") + }) + + t.Run("toggle-off drain does not block concurrent reads", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + exp := &blockingStopExporter{running: true, stopEntered: make(chan struct{}), release: make(chan struct{})} + svc.otlpExport = exp + + // Disable export while keeping a category on, so reconcileExport hits the + // drain path; run it in the background since Stop blocks. + off := &oapi.BrowserTelemetryConfig{ + Browser: &oapi.BrowserTelemetryCategoriesConfig{Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}}, + Export: &oapi.BrowserTelemetryExportConfig{Otlp: &oapi.BrowserTelemetryOTLPExportConfig{Enabled: &fa}}, + } + putDone := make(chan struct{}) + go func() { + _, _ = svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: off}) + close(putDone) + }() + + <-exp.stopEntered // drain is now in progress, holding exportMu (not monitorMu) + + // A concurrent read must not block behind the drain. + readReturned := make(chan struct{}) + go func() { + _, _ = svc.GetTelemetry(ctx, oapi.GetTelemetryRequestObject{}) + close(readReturned) + }() + select { + case <-readReturned: + case <-time.After(2 * time.Second): + t.Fatal("GET blocked on the export drain (monitorMu held during Stop)") + } + + close(exp.release) + <-putDone + }) +} + +// blockingStopExporter blocks in Stop until released, to prove the drain does +// not hold monitorMu. +type blockingStopExporter struct { + mu sync.Mutex + running bool + stopEntered chan struct{} + release chan struct{} +} + +func (e *blockingStopExporter) Start(context.Context) error { + e.mu.Lock() + defer e.mu.Unlock() + e.running = true + return nil +} + +func (e *blockingStopExporter) Stop(context.Context) error { + close(e.stopEntered) + <-e.release + e.mu.Lock() + defer e.mu.Unlock() + e.running = false + return nil +} + +func (e *blockingStopExporter) Running() bool { + e.mu.Lock() + defer e.mu.Unlock() + return e.running +} diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 0ebe40fd..19a62089 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -134,34 +134,37 @@ func main() { } // Optional OTLP export sink. Independent of S2; both can run together. - var otlpWriter *events.OTLPStorageWriter + // Constructed when an endpoint is provisioned, but left stopped: export is + // off until turned on per-session via the telemetry API, so a VM that always + // has the relay injected does not export (and get dropped) by default. + var otlpExporter api.OTLPExporter + var otlpMetrics *events.OTLPMetrics if config.OTLPEndpoint != "" { headers := map[string]string{} if config.InstanceJWT != "" { headers["Authorization"] = "Bearer " + config.InstanceJWT } - slogger.Info("OTLP export enabled", "endpoint", config.OTLPEndpoint, "path", config.OTLPPath) - otlpWriter = events.NewOTLPStorageWriter(eventStream, events.OTLPConfig{ - Endpoint: config.OTLPEndpoint, - URLPath: config.OTLPPath, - Insecure: config.OTLPInsecure, - Headers: headers, - ServiceName: config.OTLPServiceName, - InstanceName: config.InstanceName, - Metro: config.MetroName, + slogger.Info("OTLP export available", "endpoint", config.OTLPEndpoint, "path", config.OTLPPath) + // The OTel log SDK reports batch-queue drops through its global logger at + // logr V(1), which slog renders just below Info, so a default Info handler + // would swallow it. Wire it to a handler that admits that level and counts + // drops into otlpMetrics, so backpressure is both logged and scrapable. + otlpMetrics = &events.OTLPMetrics{} + otelDiag := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo - 1}) + otel.SetLogger(logr.FromSlogHandler(events.NewDropCountingHandler(otelDiag, otlpMetrics))) + otlpExporter = events.NewOTLPExportController(eventStream, events.OTLPConfig{ + Endpoint: config.OTLPEndpoint, + URLPath: config.OTLPPath, + Insecure: config.OTLPInsecure, + Headers: headers, + ServiceName: config.OTLPServiceName, + InstanceName: config.InstanceName, + Metro: config.MetroName, + MaxQueueSize: config.OTLPMaxQueueSize, + ExportInterval: config.OTLPExportInterval, + ExportTimeout: config.OTLPExportTimeout, + Metrics: otlpMetrics, }, slogger) - if err := otlpWriter.Start(ctx); err != nil { - // Best-effort sink: a failed exporter must not take down the browser. - slogger.Error("OTLP export disabled: storage writer failed to start", "err", err) - otlpWriter = nil - } else { - // Export is running, so route the OTel log SDK's global logger (which - // reports batch-queue drops at logr V(1), just below slog Info) to a - // handler that admits that level, making drops observable. Only touched - // on success, so a failed start leaves the process-wide logger alone. - otelDiag := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo - 1}) - otel.SetLogger(logr.FromSlogHandler(otelDiag)) - } } apiService, err := api.New( @@ -173,6 +176,7 @@ func main() { telemetrySession, eventStream, config.DisplayNum, + otlpExporter, ) if err != nil { slogger.Error("failed to create api service", "err", err) @@ -297,11 +301,15 @@ func main() { // logging (scrapes would drown the logs). rMetrics := chi.NewRouter() rMetrics.Use(chiMiddleware.Recoverer) - rMetrics.Method(http.MethodGet, "/metrics", metrics.Handler(slogger, + metricsCollectors := []metrics.Collector{ metrics.NewChromeCollector(upstreamMgr), metrics.NewGPUCollector(), metrics.NewSystemCollector(), - )) + } + if otlpMetrics != nil { + metricsCollectors = append(metricsCollectors, metrics.NewOTLPCollector(otlpMetrics)) + } + rMetrics.Method(http.MethodGet, "/metrics", metrics.Handler(slogger, metricsCollectors...)) srvMetrics := &http.Server{ Addr: fmt.Sprintf("0.0.0.0:%d", config.MetricsPort), Handler: rMetrics, @@ -384,13 +392,16 @@ func main() { } } - if otlpWriter != nil { + // Likewise stop OTLP export after the servers drain (a no-op if the toggle + // left it off), so shutdown-window events are exported rather than dropped. + if otlpExporter != nil { stopCtx, stopCancel := context.WithTimeout(context.Background(), 10*time.Second) defer stopCancel() - if err := otlpWriter.Stop(stopCtx); err != nil { - slogger.Error("otlp storage writer stop failed", "err", err) + if err := otlpExporter.Stop(stopCtx); err != nil { + slogger.Error("otlp export stop failed", "err", err) } } + } func mustFFmpeg() { diff --git a/server/cmd/config/config.go b/server/cmd/config/config.go index fe2240ff..70cce914 100644 --- a/server/cmd/config/config.go +++ b/server/cmd/config/config.go @@ -6,6 +6,8 @@ import ( "time" "github.com/kelseyhightower/envconfig" + + "github.com/kernel/kernel-images/server/lib/events" ) // Config holds all configuration for the server @@ -60,6 +62,14 @@ type Config struct { OTLPPath string `envconfig:"BTEL_OTLP_PATH" default:"/v1/logs"` OTLPInsecure bool `envconfig:"BTEL_OTLP_INSECURE" default:"false"` OTLPServiceName string `envconfig:"BTEL_OTLP_SERVICE_NAME" default:"kernel-browser"` + // OTLP batch tuning. Defaults match the OTel SDK's, so leaving these unset + // preserves prior behavior. MaxQueueSize is the backpressure buffer: once + // full, the oldest records are dropped (see the drop metric). It must be at + // least the export batch size (validated); a smaller queue would drop before + // a batch fills. + OTLPMaxQueueSize int `envconfig:"BTEL_OTLP_MAX_QUEUE_SIZE" default:"2048"` + OTLPExportInterval time.Duration `envconfig:"BTEL_OTLP_EXPORT_INTERVAL" default:"1s"` + OTLPExportTimeout time.Duration `envconfig:"BTEL_OTLP_EXPORT_TIMEOUT" default:"30s"` // Platform-injected identity, reused to stamp the OTLP Resource. These are // the same envs the VM already receives. InstanceJWT string `envconfig:"KERNEL_INSTANCE_JWT" default:""` @@ -101,6 +111,9 @@ func (c *Config) LogValue() slog.Value { slog.Bool("otlp_insecure", c.OTLPInsecure), slog.String("otlp_instance_jwt", otlpJWT), slog.String("otlp_service_name", c.OTLPServiceName), + slog.Int("otlp_max_queue_size", c.OTLPMaxQueueSize), + slog.Duration("otlp_export_interval", c.OTLPExportInterval), + slog.Duration("otlp_export_timeout", c.OTLPExportTimeout), slog.String("otlp_instance_name", c.InstanceName), slog.String("otlp_metro", c.MetroName), ) @@ -144,6 +157,18 @@ func validate(config *Config) error { if config.DevToolsProxyAddr == "" { return fmt.Errorf("DEVTOOLS_PROXY_ADDR is required") } + // The queue must hold at least one full export batch; a smaller value would + // drop records before a batch ever fills. Validated against the batch-size + // constant so the two can't drift. + if config.OTLPMaxQueueSize < events.DefaultOTLPMaxBatchRecords { + return fmt.Errorf("BTEL_OTLP_MAX_QUEUE_SIZE must be at least %d (the export batch size)", events.DefaultOTLPMaxBatchRecords) + } + if config.OTLPExportInterval <= 0 { + return fmt.Errorf("BTEL_OTLP_EXPORT_INTERVAL must be greater than 0") + } + if config.OTLPExportTimeout <= 0 { + return fmt.Errorf("BTEL_OTLP_EXPORT_TIMEOUT must be greater than 0") + } return nil } diff --git a/server/cmd/config/config_test.go b/server/cmd/config/config_test.go index e8674d12..eda4e366 100644 --- a/server/cmd/config/config_test.go +++ b/server/cmd/config/config_test.go @@ -34,6 +34,9 @@ func TestLoad(t *testing.T) { DevToolsProxyAddr: "127.0.0.1:9222", OTLPPath: "/v1/logs", OTLPServiceName: "kernel-browser", + OTLPMaxQueueSize: 2048, + OTLPExportInterval: time.Second, + OTLPExportTimeout: 30 * time.Second, }, }, { @@ -70,6 +73,9 @@ func TestLoad(t *testing.T) { DevToolsProxyAddr: "127.0.0.1:9876", OTLPPath: "/v1/logs", OTLPServiceName: "kernel-browser", + OTLPMaxQueueSize: 2048, + OTLPExportInterval: time.Second, + OTLPExportTimeout: 30 * time.Second, }, }, { @@ -95,6 +101,9 @@ func TestLoad(t *testing.T) { DevToolsProxyAddr: "10.0.0.1:1234", OTLPPath: "/v1/logs", OTLPServiceName: "kernel-browser", + OTLPMaxQueueSize: 2048, + OTLPExportInterval: time.Second, + OTLPExportTimeout: 30 * time.Second, }, }, { @@ -118,6 +127,20 @@ func TestLoad(t *testing.T) { }, wantErr: true, }, + { + name: "non-positive otlp queue size", + env: map[string]string{ + "BTEL_OTLP_MAX_QUEUE_SIZE": "0", + }, + wantErr: true, + }, + { + name: "otlp queue size below the export batch size", + env: map[string]string{ + "BTEL_OTLP_MAX_QUEUE_SIZE": "100", + }, + wantErr: true, + }, { name: "missing ffmpeg path (set to empty)", env: map[string]string{ diff --git a/server/lib/cdpmonitor/domains.go b/server/lib/cdpmonitor/domains.go index d8277969..00073695 100644 --- a/server/lib/cdpmonitor/domains.go +++ b/server/lib/cdpmonitor/domains.go @@ -38,6 +38,11 @@ func (m *Monitor) enableDomains(ctx context.Context, sessionID string, targetTyp m.log.Warn("cdpmonitor: failed to enable CDP domain", "method", "Page.enable", "session", sessionID, "err", err) } + // Inspector.targetCrashed reports a renderer crash on this session. + if _, err := m.send(ctx, "Inspector.enable", nil, sessionID); err != nil && ctx.Err() == nil { + m.log.Warn("cdpmonitor: failed to enable CDP domain", "method", "Inspector.enable", "session", sessionID, "err", err) + } + if _, err := m.send(ctx, "Runtime.addBinding", map[string]any{ "name": bindingName, }, sessionID); err != nil && ctx.Err() == nil { diff --git a/server/lib/cdpmonitor/handlers.go b/server/lib/cdpmonitor/handlers.go index 6c13dfc9..f551cc03 100644 --- a/server/lib/cdpmonitor/handlers.go +++ b/server/lib/cdpmonitor/handlers.go @@ -169,9 +169,39 @@ func (m *Monitor) dispatchEvent(msg cdpMessage) { if m.decodeParams(msg.Method, msg.Params, &p) { m.handleDetachedFromTarget(p) } + case "Inspector.targetCrashed": + // No params; the crashed page is identified by the session it fires on. + m.handleTargetCrashed(msg.SessionID) } } +// handleTargetCrashed publishes a page_crashed event for a renderer crash on +// sessionID. The URL comes from the tracked target info; target id and type are +// stamped into source metadata by publishEvent. +func (m *Monitor) handleTargetCrashed(sessionID string) { + m.sessionsMu.RLock() + info, tracked := m.sessions[sessionID] + m.sessionsMu.RUnlock() + + // A crash on a session we never attached to shouldn't happen, but if it + // does, still emit the event (the crash is real) rather than dropping it. + // The tracked target info is empty in that case; fall back to "other" so + // target_type stays a valid BrowserTargetType rather than an empty string. + if !tracked { + m.log.Warn("cdpmonitor: target crashed on untracked session", "session", sessionID) + } + targetType := oapi.BrowserTargetType(info.targetType) + if targetType == "" { + targetType = oapi.BrowserTargetTypeOther + } + data, _ := json.Marshal(oapi.BrowserPageCrashedEventData{ + TargetId: info.targetID, + TargetType: targetType, + Url: info.url, + }) + m.publishEvent(EventPageCrashed, events.Page, oapi.BrowserEventSource{Kind: oapi.Cdp}, "Inspector.targetCrashed", data, sessionID) +} + func (m *Monitor) handleConsole(p cdpRuntimeConsoleAPICalledParams, sessionID string) { text := "" if len(p.Args) > 0 { diff --git a/server/lib/cdpmonitor/handlers_test.go b/server/lib/cdpmonitor/handlers_test.go index d9c760d5..7cd1b5a9 100644 --- a/server/lib/cdpmonitor/handlers_test.go +++ b/server/lib/cdpmonitor/handlers_test.go @@ -334,6 +334,64 @@ func TestTabOpened(t *testing.T) { }) } +func TestTargetCrashed(t *testing.T) { + srv := newTestServer(t) + defer srv.close() + + _, ec, cleanup := startMonitor(t, srv, nil) + defer cleanup() + + srv.sendToMonitor(t, map[string]any{ + "method": "Target.attachedToTarget", + "params": map[string]any{ + "sessionId": "sess-crash", + "targetInfo": map[string]any{ + "targetId": "target-crash", "type": "page", + "url": "https://crash.example.com", "attached": true, + }, + "waitingForDebugger": false, + }, + }) + ec.waitFor(t, "page_tab_opened", 2*time.Second) + + srv.sendToMonitor(t, map[string]any{ + "method": "Inspector.targetCrashed", + "sessionId": "sess-crash", + "params": map[string]any{}, + }) + ev := ec.waitFor(t, "page_crashed", 2*time.Second) + assert.Equal(t, events.Page, ev.Category) + assert.Equal(t, "Inspector.targetCrashed", *ev.Source.Event) + var data map[string]any + require.NoError(t, json.Unmarshal(ev.Data, &data)) + assert.Equal(t, "target-crash", data["target_id"]) + assert.Equal(t, "page", data["target_type"]) + assert.Equal(t, "https://crash.example.com", data["url"]) +} + +// A crash on a session the monitor never attached to still emits page_crashed, +// but target_type must stay a valid enum ("other"), not an empty string. +func TestTargetCrashedUntracked(t *testing.T) { + srv := newTestServer(t) + defer srv.close() + + _, ec, cleanup := startMonitor(t, srv, nil) + defer cleanup() + + srv.sendToMonitor(t, map[string]any{ + "method": "Inspector.targetCrashed", + "sessionId": "sess-unknown", + "params": map[string]any{}, + }) + ev := ec.waitFor(t, "page_crashed", 2*time.Second) + assert.Equal(t, events.Page, ev.Category) + var data map[string]any + require.NoError(t, json.Unmarshal(ev.Data, &data)) + assert.Equal(t, "other", data["target_type"]) + assert.Equal(t, "", data["target_id"]) + assert.Equal(t, "", data["url"]) +} + func TestBindingAndTimeline(t *testing.T) { withMonitor := func(t *testing.T) (*testServer, *eventCollector) { t.Helper() diff --git a/server/lib/cdpmonitor/types.go b/server/lib/cdpmonitor/types.go index 1cdce741..83eff1e2 100644 --- a/server/lib/cdpmonitor/types.go +++ b/server/lib/cdpmonitor/types.go @@ -27,6 +27,7 @@ const ( EventLayoutShift = "page_layout_shift" // PerformanceTimeline event of type "layout-shift" EventLCP = "page_lcp" // PerformanceTimeline event of type "largest-contentful-paint" EventTabOpened = "page_tab_opened" // Target.attachedToTarget for type=page + EventPageCrashed = "page_crashed" // Inspector.targetCrashed (renderer process crash) ) // Computed events — synthetic events derived by computed.go state machines. diff --git a/server/lib/events/category_gen.go b/server/lib/events/category_gen.go index 06d641fb..b02132f9 100644 --- a/server/lib/events/category_gen.go +++ b/server/lib/events/category_gen.go @@ -25,6 +25,7 @@ var categoryByType = map[string]oapi.TelemetryEventCategory{ "network_loading_failed": oapi.TelemetryEventCategory("network"), "network_request": oapi.TelemetryEventCategory("network"), "network_response": oapi.TelemetryEventCategory("network"), + "page_crashed": oapi.TelemetryEventCategory("page"), "page_dom_content_loaded": oapi.TelemetryEventCategory("page"), "page_layout_settled": oapi.TelemetryEventCategory("page"), "page_layout_shift": oapi.TelemetryEventCategory("page"), diff --git a/server/lib/events/eventsstorage.go b/server/lib/events/eventsstorage.go index 73065eec..c14cf063 100644 --- a/server/lib/events/eventsstorage.go +++ b/server/lib/events/eventsstorage.go @@ -26,10 +26,19 @@ type StorageWriter struct { appendErrors atomic.Uint64 // total append failures; best-effort, not retried } -// NewStorageWriter creates a writer that reads from es starting at seq 0. +// NewStorageWriter creates a writer that reads from es starting at the oldest +// buffered event (seq 0). func NewStorageWriter(es *EventStream, storage Storage, log *slog.Logger) *StorageWriter { + return NewStorageWriterAfter(es, storage, log, 0) +} + +// NewStorageWriterAfter creates a writer that reads from es starting after +// afterSeq. Pass 0 to start from the oldest buffered event; pass the stream's +// current seq to start from the tail (only future events), so a writer that is +// rebuilt on demand does not replay the ring. +func NewStorageWriterAfter(es *EventStream, storage Storage, log *slog.Logger, afterSeq uint64) *StorageWriter { return &StorageWriter{ - reader: es.NewReader(0), + reader: es.NewReader(afterSeq), storage: storage, log: log, } diff --git a/server/lib/events/otlpconvert.go b/server/lib/events/otlpconvert.go index a49ed4fa..2b30d27a 100644 --- a/server/lib/events/otlpconvert.go +++ b/server/lib/events/otlpconvert.go @@ -127,6 +127,10 @@ func otlpSeverity(eventType string) (log.Severity, string) { switch { case eventType == "console_error": return log.SeverityError, "ERROR" + // Process-death, renderer-crash, and OOM events are failures a consumer + // alerts on, so they map to ERROR rather than the default INFO. + case eventType == "service_crashed", eventType == "system_oom_kill", eventType == "page_crashed": + return log.SeverityError, "ERROR" case strings.HasSuffix(eventType, "_failed"): return log.SeverityWarn, "WARN" default: diff --git a/server/lib/events/otlpconvert_test.go b/server/lib/events/otlpconvert_test.go index 83d8718f..4c5621a6 100644 --- a/server/lib/events/otlpconvert_test.go +++ b/server/lib/events/otlpconvert_test.go @@ -113,6 +113,8 @@ func TestToLogRecord_StructuredBody(t *testing.T) { func TestToLogRecord_Severity(t *testing.T) { cases := map[string]log.Severity{ "console_error": log.SeverityError, + "service_crashed": log.SeverityError, + "system_oom_kill": log.SeverityError, "network_loading_failed": log.SeverityWarn, "monitor_init_failed": log.SeverityWarn, "network_response": log.SeverityInfo, diff --git a/server/lib/events/otlpmetrics.go b/server/lib/events/otlpmetrics.go new file mode 100644 index 00000000..08b38dcb --- /dev/null +++ b/server/lib/events/otlpmetrics.go @@ -0,0 +1,88 @@ +package events + +import ( + "context" + "log/slog" + "sync/atomic" +) + +// OTLPMetrics holds process-lifetime counters for the OTLP export sink. It is +// owned above the export writer so the counts stay monotonic across export +// enable/disable cycles (the writer is rebuilt on each enable). Safe for +// concurrent use. +type OTLPMetrics struct { + dropped atomic.Uint64 + failures atomic.Uint64 + exported atomic.Uint64 +} + +// Dropped returns records dropped from the batch queue under backpressure. +func (m *OTLPMetrics) Dropped() uint64 { return m.dropped.Load() } + +// Failures returns export requests that returned an error. +func (m *OTLPMetrics) Failures() uint64 { return m.failures.Load() } + +// Exported returns records that were successfully exported. +func (m *OTLPMetrics) Exported() uint64 { return m.exported.Load() } + +// The OTel batch log processor reports queue-overflow drops only through its +// global logger as `Warn("dropped log records", "dropped", n)` and resets its +// internal counter each report, so this message is the sole place the count is +// observable. dropCountingHandler reads n off that record into an OTLPMetrics. +const ( + otlpDroppedLogMsg = "dropped log records" + otlpDroppedLogAttr = "dropped" +) + +type dropCountingHandler struct { + slog.Handler + metrics *OTLPMetrics +} + +// NewDropCountingHandler wraps base so OTel batch-queue drop reports are summed +// into metrics before being logged as usual. +func NewDropCountingHandler(base slog.Handler, metrics *OTLPMetrics) slog.Handler { + return &dropCountingHandler{Handler: base, metrics: metrics} +} + +func (h *dropCountingHandler) Handle(ctx context.Context, r slog.Record) error { + if r.Message == otlpDroppedLogMsg { + r.Attrs(func(a slog.Attr) bool { + if a.Key != otlpDroppedLogAttr { + return true + } + if n := attrUint64(a.Value); n > 0 { + h.metrics.dropped.Add(n) + } + return false + }) + } + return h.Handler.Handle(ctx, r) +} + +// attrUint64 reads a non-negative integer from a slog value regardless of how +// the logr bridge typed it (uint64, int64, or a boxed int). +func attrUint64(v slog.Value) uint64 { + switch v.Kind() { + case slog.KindUint64: + return v.Uint64() + case slog.KindInt64: + if n := v.Int64(); n > 0 { + return uint64(n) + } + default: + switch x := v.Any().(type) { + case uint64: + return x + case int64: + if x > 0 { + return uint64(x) + } + case int: + if x > 0 { + return uint64(x) + } + } + } + return 0 +} diff --git a/server/lib/events/otlpmetrics_test.go b/server/lib/events/otlpmetrics_test.go new file mode 100644 index 00000000..ac70e739 --- /dev/null +++ b/server/lib/events/otlpmetrics_test.go @@ -0,0 +1,85 @@ +package events + +import ( + "context" + "io" + "log/slog" + "net" + "testing" + "time" + + "github.com/go-logr/logr" + oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" +) + +// TestDropCountingHandler_CountsDrops confirms the handler sums the SDK's +// drop-report count and ignores unrelated log records. +func TestDropCountingHandler_CountsDrops(t *testing.T) { + m := &OTLPMetrics{} + h := NewDropCountingHandler(slog.NewTextHandler(discard{}, nil), m) + log := slog.New(h) + + log.Warn(otlpDroppedLogMsg, otlpDroppedLogAttr, uint64(5)) + log.Warn(otlpDroppedLogMsg, otlpDroppedLogAttr, 3) // boxed int, as the logr bridge may pass it + log.Info("something else", "dropped", 99) // unrelated message must not count + + assert.Equal(t, uint64(8), m.Dropped()) +} + +type discard struct{} + +func (discard) Write(p []byte) (int, error) { return len(p), nil } + +// TestOTLPMetrics_CountsRealDrops forces a real batch-queue overflow through +// the OTel SDK and asserts the drop count reaches OTLPMetrics via the global +// logger. This exercises the full SDK -> global logger -> counting handler +// chain every run, so an SDK bump that renames the drop-report message fails +// here instead of silently zeroing the metric. No external services: a local +// listener accepts and hangs so each export blocks and the tiny queue overflows. +func TestOTLPMetrics_CountsRealDrops(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + go func() { + for { + c, err := ln.Accept() + if err != nil { + return + } + _ = c // hold open, never respond + } + }() + + m := &OTLPMetrics{} + // The SDK reports drops just below Info, so the base handler must admit that + // level or slog short-circuits before the counter runs (mirrors main.go). + base := slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelInfo - 1}) + otel.SetLogger(logr.FromSlogHandler(NewDropCountingHandler(base, m))) + t.Cleanup(func() { otel.SetLogger(logr.Discard()) }) + + es, err := NewEventStream(EventStreamConfig{RingCapacity: 4096}) + require.NoError(t, err) + ctrl := NewOTLPExportController(es, OTLPConfig{ + Endpoint: ln.Addr().String(), + Insecure: true, + MaxQueueSize: 4, // tiny buffer overflows while the first export blocks + ExportInterval: 50 * time.Millisecond, + ExportTimeout: time.Second, + Metrics: m, + }, slog.New(slog.NewTextHandler(io.Discard, nil))) + require.NoError(t, ctrl.Start(context.Background())) + t.Cleanup(func() { + stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = ctrl.Stop(stopCtx) + }) + + for i := 0; i < 2000; i++ { + es.Publish(Envelope{Event: Event{Ts: time.Now().UnixMicro(), Type: "network_response", Category: Network, Source: oapi.BrowserEventSource{Kind: oapi.Cdp}, Data: []byte(`{"status":200}`)}}) + } + require.Eventually(t, func() bool { return m.Dropped() > 0 }, 4*time.Second, 100*time.Millisecond, + "queue overflow should be counted as drops via the SDK global logger") +} diff --git a/server/lib/events/otlpstorage.go b/server/lib/events/otlpstorage.go index 66e2f88a..3e7f18a3 100644 --- a/server/lib/events/otlpstorage.go +++ b/server/lib/events/otlpstorage.go @@ -5,7 +5,7 @@ import ( "fmt" "log/slog" "sync" - "sync/atomic" + "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" @@ -15,11 +15,13 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.27.0" ) -// defaultOTLPMaxBatchRecords bounds how many records the SDK buffers per export +// DefaultOTLPMaxBatchRecords bounds how many records the SDK buffers per export // cycle. The per-request byte size is bounded separately by the loggingExporter, // which splits an export into sub-requests under maxOTLPExportBytes so a batch of -// large records can't exceed the target's HTTP body limit. -const defaultOTLPMaxBatchRecords = 200 +// large records can't exceed the target's HTTP body limit. Exported so the +// config layer can validate the queue size against it (the queue must hold at +// least one full batch). +const DefaultOTLPMaxBatchRecords = 200 // maxOTLPExportBytes caps the estimated payload of a single export request so it // stays well under common OTLP/HTTP body limits (collectors default to ~20MiB). @@ -27,6 +29,10 @@ const defaultOTLPMaxBatchRecords = 200 // record fits within one sub-request. const maxOTLPExportBytes = 4 << 20 // 4 MiB +// otlpForceCloseTimeout bounds the provider shutdown when Stop's read-loop wait +// has already exhausted its ctx, so a hung flush can't block indefinitely. +const otlpForceCloseTimeout = 2 * time.Second + // OTLPConfig configures the OTLP telemetry sink. type OTLPConfig struct { // Endpoint is the host[:port] of the OTLP/HTTP target: a collector in @@ -44,31 +50,41 @@ type OTLPConfig struct { ServiceName string InstanceName string Metro string + + // Batch tuning. Zero values fall back to the SDK defaults. + MaxQueueSize int + ExportInterval time.Duration + ExportTimeout time.Duration + + // Metrics accumulates export counters. When nil a throwaway is used so the + // sink still runs; callers that scrape metrics pass a shared instance. + Metrics *OTLPMetrics } // loggingExporter wraps the OTLP exporter to (1) split each export into // sub-requests under maxOTLPExportBytes so a batch of large records can't exceed -// the target's HTTP body limit, and (2) surface export-time failures (network / -// customer-endpoint errors) with a running failure count. Queue drops under -// sustained backpressure are best-effort (the SDK drops the oldest record, -// bounded by the batch queue size) and are reported separately by the SDK's -// global logger, which main wires to our logger so they are observable. +// the target's HTTP body limit, and (2) record export outcomes into the shared +// OTLPMetrics (failures and successfully exported records). Queue drops under +// sustained backpressure are counted separately, via the SDK's global logger, +// which main wires through NewDropCountingHandler. type loggingExporter struct { sdklog.Exporter - log *slog.Logger - failures atomic.Uint64 + log *slog.Logger + metrics *OTLPMetrics } func (e *loggingExporter) Export(ctx context.Context, records []sdklog.Record) error { var firstErr error for _, chunk := range chunkBySize(records, maxOTLPExportBytes) { if err := e.Exporter.Export(ctx, chunk); err != nil { - n := e.failures.Add(1) + n := e.metrics.failures.Add(1) e.log.Warn("otlp export failed", "records", len(chunk), "err", err, "total_failures", n) if firstErr == nil { firstErr = err } + continue } + e.metrics.exported.Add(uint64(len(chunk))) } return firstErr } @@ -166,9 +182,23 @@ func newOTLPStorage(ctx context.Context, cfg OTLPConfig, log *slog.Logger) (*otl } res := resource.NewSchemaless(attrs...) + procOpts := []sdklog.BatchProcessorOption{sdklog.WithExportMaxBatchSize(DefaultOTLPMaxBatchRecords)} + if cfg.MaxQueueSize > 0 { + procOpts = append(procOpts, sdklog.WithMaxQueueSize(cfg.MaxQueueSize)) + } + if cfg.ExportInterval > 0 { + procOpts = append(procOpts, sdklog.WithExportInterval(cfg.ExportInterval)) + } + if cfg.ExportTimeout > 0 { + procOpts = append(procOpts, sdklog.WithExportTimeout(cfg.ExportTimeout)) + } + metrics := cfg.Metrics + if metrics == nil { + metrics = &OTLPMetrics{} + } processor := sdklog.NewBatchProcessor( - &loggingExporter{Exporter: exporter, log: log}, - sdklog.WithExportMaxBatchSize(defaultOTLPMaxBatchRecords), + &loggingExporter{Exporter: exporter, log: log, metrics: metrics}, + procOpts..., ) provider := sdklog.NewLoggerProvider( sdklog.WithResource(res), @@ -199,7 +229,9 @@ func (s *otlpStorage) Close(ctx context.Context) error { // OTLPStorageWriter reads from an EventStream and forwards each event to an // OTLP endpoint. Construct with NewOTLPStorageWriter, call Start to begin and -// Stop to drain and shut down. Mirrors S2StorageWriter. +// Stop to drain and shut down. Unlike S2StorageWriter it starts from the stream +// tail at Start, so a writer rebuilt on a runtime export re-enable forwards only +// new events rather than replaying the retained ring. type OTLPStorageWriter struct { es *EventStream cfg OTLPConfig @@ -230,7 +262,11 @@ func (w *OTLPStorageWriter) Start(ctx context.Context) error { return err } w.storage = storage - w.writer = NewStorageWriter(w.es, storage, w.log) + // Start from the current tail, not seq 0: export is toggled at runtime and + // the writer is rebuilt on each enable, so reading from the oldest buffered + // event would re-export the retained ring every time it is turned back on. + // Only events published while export is on are forwarded. + w.writer = NewStorageWriterAfter(w.es, storage, w.log, w.es.Seq()) w.done = make(chan struct{}) w.started = true go func() { @@ -255,6 +291,12 @@ func (w *OTLPStorageWriter) Stop(ctx context.Context) error { select { case <-w.done: case <-ctx.Done(): + // The read loop didn't exit in time. Still shut the provider down so its + // batch-export goroutines don't leak; detach from the expired ctx to give + // Shutdown a brief window to flush. + closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), otlpForceCloseTimeout) + defer cancel() + _ = w.storage.Close(closeCtx) return ctx.Err() } if err := w.writer.Drain(ctx); err != nil { @@ -262,3 +304,60 @@ func (w *OTLPStorageWriter) Stop(ctx context.Context) error { } return w.storage.Close(ctx) } + +// OTLPExportController starts and stops OTLP export on demand so it can be +// toggled at runtime via the telemetry API. The underlying writer is one-shot, +// so each enable builds a fresh one. Safe for concurrent use. +type OTLPExportController struct { + es *EventStream + cfg OTLPConfig + log *slog.Logger + + mu sync.Mutex + writer *OTLPStorageWriter + cancel context.CancelFunc +} + +func NewOTLPExportController(es *EventStream, cfg OTLPConfig, log *slog.Logger) *OTLPExportController { + return &OTLPExportController{es: es, cfg: cfg, log: log} +} + +// Start begins export, or is a no-op if already running. parent governs the +// read loop; the controller derives a cancelable child so Stop can halt the +// loop even when parent is still live (a runtime toggle-off). +func (c *OTLPExportController) Start(parent context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.writer != nil { + return nil + } + runCtx, cancel := context.WithCancel(parent) + w := NewOTLPStorageWriter(c.es, c.cfg, c.log) + if err := w.Start(runCtx); err != nil { + cancel() + return err + } + c.writer, c.cancel = w, cancel + return nil +} + +// Stop drains and shuts down a running exporter, or is a no-op if stopped. ctx +// bounds shutdown time. +func (c *OTLPExportController) Stop(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.writer == nil { + return nil + } + c.cancel() + err := c.writer.Stop(ctx) + c.writer, c.cancel = nil, nil + return err +} + +// Running reports whether export is currently active. +func (c *OTLPExportController) Running() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.writer != nil +} diff --git a/server/lib/events/otlpstorage_test.go b/server/lib/events/otlpstorage_test.go index 1fae232f..50eecc8e 100644 --- a/server/lib/events/otlpstorage_test.go +++ b/server/lib/events/otlpstorage_test.go @@ -27,18 +27,22 @@ func (s stubExporter) Export(context.Context, []sdklog.Record) error { return s. func (s stubExporter) Shutdown(context.Context) error { return nil } func (s stubExporter) ForceFlush(context.Context) error { return nil } -// TestLoggingExporter_CountsExportFailures confirms the wrapper surfaces export -// failures (the reason it exists) and leaves the counter untouched on success. +// TestLoggingExporter_CountsExportFailures confirms the wrapper records export +// failures and successfully exported records into the shared metrics. func TestLoggingExporter_CountsExportFailures(t *testing.T) { recs := []sdklog.Record{recordOfSize(10)} - failing := &loggingExporter{Exporter: stubExporter{err: errors.New("boom")}, log: slog.Default()} + fm := &OTLPMetrics{} + failing := &loggingExporter{Exporter: stubExporter{err: errors.New("boom")}, log: slog.Default(), metrics: fm} require.Error(t, failing.Export(context.Background(), recs)) require.Error(t, failing.Export(context.Background(), recs)) - assert.Equal(t, uint64(2), failing.failures.Load()) + assert.Equal(t, uint64(2), fm.Failures()) + assert.Equal(t, uint64(0), fm.Exported()) - ok := &loggingExporter{Exporter: stubExporter{}, log: slog.Default()} + om := &OTLPMetrics{} + ok := &loggingExporter{Exporter: stubExporter{}, log: slog.Default(), metrics: om} require.NoError(t, ok.Export(context.Background(), recs)) - assert.Equal(t, uint64(0), ok.failures.Load()) + assert.Equal(t, uint64(0), om.Failures()) + assert.Equal(t, uint64(1), om.Exported()) } // TestOTLPStorageWriter_ExportsEvents drives the full sink against a local HTTP @@ -121,6 +125,71 @@ func TestOTLPStorageWriter_ExportsEvents(t *testing.T) { assert.True(t, bodyIsMap, "structured body should arrive as a kvlist") } +// TestOTLPExportController_NoReplayOnReenable confirms that toggling export off +// and back on does not re-export events already sent, and does not export events +// published while it was off: the rebuilt writer starts from the stream tail. +func TestOTLPExportController_NoReplayOnReenable(t *testing.T) { + var mu sync.Mutex + var urls []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req collogspb.ExportLogsServiceRequest + require.NoError(t, proto.Unmarshal(body, &req)) + mu.Lock() + for _, rl := range req.ResourceLogs { + for _, sl := range rl.ScopeLogs { + for _, lr := range sl.LogRecords { + for _, kv := range lr.Attributes { + if kv.Key == "url.full" { + urls = append(urls, kv.Value.GetStringValue()) + } + } + } + } + } + mu.Unlock() + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + es, err := NewEventStream(EventStreamConfig{RingCapacity: 64}) + require.NoError(t, err) + + publish := func(url string) { + es.Publish(Envelope{Event: Event{Ts: 1, Type: "network_response", Category: Network, + Data: []byte(`{"method":"GET","url":"` + url + `","status":200}`)}}) + } + + ctrl := NewOTLPExportController(es, OTLPConfig{ + Endpoint: strings.TrimPrefix(srv.URL, "http://"), + URLPath: "/v1/logs", + Insecure: true, + }, slog.Default()) + + // First enable: only events published while on are exported. + require.NoError(t, ctrl.Start(context.Background())) + publish("https://one") + stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, ctrl.Stop(stopCtx)) + + // Published while off: must never be exported. + publish("https://gap") + + // Re-enable: must not replay "one" or "gap", only forward new events. + require.NoError(t, ctrl.Start(context.Background())) + publish("https://two") + stopCtx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel2() + require.NoError(t, ctrl.Stop(stopCtx2)) + + mu.Lock() + defer mu.Unlock() + assert.ElementsMatch(t, []string{"https://one", "https://two"}, urls, + "each event exported exactly once; no ring replay and no off-window events") +} + // recordOfSize builds a log record whose body string is n bytes, so // estimateRecordBytes reports ~n. func recordOfSize(n int) sdklog.Record { diff --git a/server/lib/metrics/otlp.go b/server/lib/metrics/otlp.go new file mode 100644 index 00000000..a4f3efe9 --- /dev/null +++ b/server/lib/metrics/otlp.go @@ -0,0 +1,37 @@ +package metrics + +import "context" + +// OTLPSource exposes the OTLP export sink's lifetime counters. Implemented by +// *events.OTLPMetrics. +type OTLPSource interface { + Dropped() uint64 + Failures() uint64 + Exported() uint64 +} + +// OTLPCollector reports OTLP export health: records dropped under backpressure, +// failed export requests, and records successfully exported. All are +// process-lifetime counters with no labels; the scraper attaches instance +// identity. +type OTLPCollector struct { + src OTLPSource +} + +func NewOTLPCollector(src OTLPSource) *OTLPCollector { + return &OTLPCollector{src: src} +} + +func (c *OTLPCollector) Name() string { return "otlp" } + +func (c *OTLPCollector) Collect(_ context.Context, w *Writer) error { + w.Metric("kernel_otlp_records_dropped_total", "OTLP telemetry records dropped due to export queue overflow.", "counter") + w.Sample("kernel_otlp_records_dropped_total", nil, float64(c.src.Dropped())) + + w.Metric("kernel_otlp_export_failures_total", "OTLP export requests that returned an error.", "counter") + w.Sample("kernel_otlp_export_failures_total", nil, float64(c.src.Failures())) + + w.Metric("kernel_otlp_records_exported_total", "OTLP telemetry records successfully exported.", "counter") + w.Sample("kernel_otlp_records_exported_total", nil, float64(c.src.Exported())) + return nil +} diff --git a/server/lib/metrics/otlp_test.go b/server/lib/metrics/otlp_test.go new file mode 100644 index 00000000..9622b5ef --- /dev/null +++ b/server/lib/metrics/otlp_test.go @@ -0,0 +1,29 @@ +package metrics + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type stubOTLPSource struct{ dropped, failures, exported uint64 } + +func (s stubOTLPSource) Dropped() uint64 { return s.dropped } +func (s stubOTLPSource) Failures() uint64 { return s.failures } +func (s stubOTLPSource) Exported() uint64 { return s.exported } + +func TestOTLPCollector(t *testing.T) { + w := &Writer{} + err := NewOTLPCollector(stubOTLPSource{dropped: 7, failures: 2, exported: 42}).Collect(context.Background(), w) + require.NoError(t, err) + + out := string(w.Bytes()) + assert.Contains(t, out, "kernel_otlp_records_dropped_total 7") + assert.Contains(t, out, "kernel_otlp_export_failures_total 2") + assert.Contains(t, out, "kernel_otlp_records_exported_total 42") + assert.Contains(t, out, "# TYPE kernel_otlp_records_dropped_total counter") + assert.Equal(t, 1, strings.Count(out, "# TYPE kernel_otlp_records_dropped_total")) +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index a65298b0..1957ff7c 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -758,6 +758,36 @@ func (e BrowserNetworkResponseEventType) Valid() bool { } } +// Defines values for BrowserPageCrashedEventCategory. +const ( + BrowserPageCrashedEventCategoryPage BrowserPageCrashedEventCategory = "page" +) + +// Valid indicates whether the value is a known member of the BrowserPageCrashedEventCategory enum. +func (e BrowserPageCrashedEventCategory) Valid() bool { + switch e { + case BrowserPageCrashedEventCategoryPage: + return true + default: + return false + } +} + +// Defines values for BrowserPageCrashedEventType. +const ( + PageCrashed BrowserPageCrashedEventType = "page_crashed" +) + +// Valid indicates whether the value is a known member of the BrowserPageCrashedEventType enum. +func (e BrowserPageCrashedEventType) Valid() bool { + switch e { + case PageCrashed: + return true + default: + return false + } +} + // Defines values for BrowserPageDomContentLoadedEventCategory. const ( BrowserPageDomContentLoadedEventCategoryPage BrowserPageDomContentLoadedEventCategory = "page" @@ -970,13 +1000,13 @@ func (e BrowserPageNavigationSettledEventType) Valid() bool { // Defines values for BrowserPageTabOpenedEventCategory. const ( - BrowserPageTabOpenedEventCategoryPage BrowserPageTabOpenedEventCategory = "page" + Page BrowserPageTabOpenedEventCategory = "page" ) // Valid indicates whether the value is a known member of the BrowserPageTabOpenedEventCategory enum. func (e BrowserPageTabOpenedEventCategory) Valid() bool { switch e { - case BrowserPageTabOpenedEventCategoryPage: + case Page: return true default: return false @@ -2514,6 +2544,40 @@ type BrowserNetworkResponseEventData struct { Url *string `json:"url,omitempty"` } +// BrowserPageCrashedEvent A page's renderer process crashed (an "Aw, Snap!" failure) while the browser process itself stayed alive. Reported on the crashed page's session, with the session and target ids on `source.metadata`. Captured only while the `page` category is enabled. +type BrowserPageCrashedEvent struct { + Category BrowserPageCrashedEventCategory `json:"category"` + Data *BrowserPageCrashedEventData `json:"data,omitempty"` + + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserPageCrashedEventType `json:"type"` +} + +// BrowserPageCrashedEventCategory defines model for BrowserPageCrashedEvent.Category. +type BrowserPageCrashedEventCategory string + +// BrowserPageCrashedEventType defines model for BrowserPageCrashedEvent.Type. +type BrowserPageCrashedEventType string + +// BrowserPageCrashedEventData defines model for BrowserPageCrashedEventData. +type BrowserPageCrashedEventData struct { + // TargetId CDP target identifier of the crashed page. + TargetId string `json:"target_id"` + + // TargetType CDP target type of the page that produced the event. + TargetType BrowserTargetType `json:"target_type"` + + // Url URL the page was on when its renderer process crashed. + Url string `json:"url"` +} + // BrowserPageDomContentLoadedEvent A browser DOMContentLoaded event (CDP Page.domContentEventFired). type BrowserPageDomContentLoadedEvent struct { Category BrowserPageDomContentLoadedEventCategory `json:"category"` @@ -3034,6 +3098,21 @@ type BrowserTelemetryCategoryConfig struct { type BrowserTelemetryConfig struct { // Browser Per-category telemetry capture settings for browser events. Browser *BrowserTelemetryCategoriesConfig `json:"browser,omitempty"` + + // Export Forwarding of captured telemetry to an external destination. Independent of what is captured: export is off unless explicitly enabled here, even when an export destination is configured. In a PUT (full replace) an omitted export block resets export to off, the same as omitted categories turn off; in a PATCH an omitted field leaves the current setting unchanged. + Export *BrowserTelemetryExportConfig `json:"export,omitempty"` +} + +// BrowserTelemetryExportConfig Forwarding of captured telemetry to an external destination. Independent of what is captured: export is off unless explicitly enabled here, even when an export destination is configured. In a PUT (full replace) an omitted export block resets export to off, the same as omitted categories turn off; in a PATCH an omitted field leaves the current setting unchanged. +type BrowserTelemetryExportConfig struct { + // Otlp OTLP/HTTP export settings. + Otlp *BrowserTelemetryOTLPExportConfig `json:"otlp,omitempty"` +} + +// BrowserTelemetryOTLPExportConfig OTLP/HTTP export settings. +type BrowserTelemetryOTLPExportConfig struct { + // Enabled Whether captured telemetry is forwarded to the configured OTLP destination. Off by default. Has no effect (export stays inactive) when no export destination is configured. + Enabled *bool `json:"enabled,omitempty"` } // ChromiumConfigureError Failure from batched chromium configure — includes which phase failed. @@ -4250,6 +4329,34 @@ func (t *KnownBrowserTelemetryEvent) MergeBrowserPageTabOpenedEvent(v BrowserPag return err } +// AsBrowserPageCrashedEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserPageCrashedEvent +func (t KnownBrowserTelemetryEvent) AsBrowserPageCrashedEvent() (BrowserPageCrashedEvent, error) { + var body BrowserPageCrashedEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserPageCrashedEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserPageCrashedEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserPageCrashedEvent(v BrowserPageCrashedEvent) error { + v.Type = "page_crashed" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserPageCrashedEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserPageCrashedEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserPageCrashedEvent(v BrowserPageCrashedEvent) error { + v.Type = "page_crashed" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + // AsBrowserPageLayoutShiftEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserPageLayoutShiftEvent func (t KnownBrowserTelemetryEvent) AsBrowserPageLayoutShiftEvent() (BrowserPageLayoutShiftEvent, error) { var body BrowserPageLayoutShiftEvent @@ -4864,6 +4971,8 @@ func (t KnownBrowserTelemetryEvent) ValueByDiscriminator() (interface{}, error) return t.AsBrowserNetworkRequestEvent() case "network_response": return t.AsBrowserNetworkResponseEvent() + case "page_crashed": + return t.AsBrowserPageCrashedEvent() case "page_dom_content_loaded": return t.AsBrowserPageDomContentLoadedEvent() case "page_layout_settled": @@ -18844,367 +18953,375 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "B+eOHj0HcKP/3wAAAP//AuTSjPPJAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/telemetry/telemetry.go b/server/lib/telemetry/telemetry.go index 576c8ecd..de41f880 100644 --- a/server/lib/telemetry/telemetry.go +++ b/server/lib/telemetry/telemetry.go @@ -15,6 +15,9 @@ type TelemetryConfig struct { // captures events.DefaultCategories. Monitor is added automatically when // any CDP category is present and is not configurable here. Categories []oapi.TelemetryEventCategory + // ExportOTLP forwards captured events to the configured OTLP endpoint. + // Off by default and independent of what is captured. + ExportOTLP bool } // TelemetrySession manages a telemetry session against a shared EventStream. @@ -31,6 +34,7 @@ type TelemetrySession struct { id string sessionStartSeq uint64 categories map[oapi.TelemetryEventCategory]struct{} + exportOTLP bool appliedAt time.Time } @@ -68,6 +72,7 @@ func (s *TelemetrySession) Start(telemetrySessionID string, cfg TelemetryConfig) s.sessionStartSeq = s.es.Seq() s.appliedAt = time.Now() s.categories = categorySet(cfg.Categories) + s.exportOTLP = cfg.ExportOTLP } // publishLocked stamps telemetry_session_id into ev.Source.Metadata and forwards to the bus. @@ -133,7 +138,7 @@ func (s *TelemetrySession) Config() TelemetryConfig { for c := range s.categories { cats = append(cats, c) } - return TelemetryConfig{Categories: cats} + return TelemetryConfig{Categories: cats, ExportOTLP: s.exportOTLP} } // AppliedAt returns when the current configuration was applied, or the zero @@ -149,6 +154,7 @@ func (s *TelemetrySession) UpdateConfig(cfg TelemetryConfig) { s.mu.Lock() defer s.mu.Unlock() s.categories = categorySet(cfg.Categories) + s.exportOTLP = cfg.ExportOTLP } // CategoryEnabled reports whether events in category c are currently captured. @@ -177,4 +183,7 @@ func (s *TelemetrySession) Stop() { defer s.mu.Unlock() s.id = "" s.appliedAt = time.Time{} + // The session is over, so export is off; keep Config() authoritative for the + // desired export state after a clear. + s.exportOTLP = false } diff --git a/server/openapi.yaml b/server/openapi.yaml index 4f56b0e6..5ef7d60e 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1592,6 +1592,31 @@ components: properties: browser: $ref: "#/components/schemas/BrowserTelemetryCategoriesConfig" + export: + $ref: "#/components/schemas/BrowserTelemetryExportConfig" + additionalProperties: false + BrowserTelemetryExportConfig: + type: object + description: > + Forwarding of captured telemetry to an external destination. Independent + of what is captured: export is off unless explicitly enabled here, even + when an export destination is configured. In a PUT (full replace) an + omitted export block resets export to off, the same as omitted categories + turn off; in a PATCH an omitted field leaves the current setting unchanged. + properties: + otlp: + $ref: "#/components/schemas/BrowserTelemetryOTLPExportConfig" + additionalProperties: false + BrowserTelemetryOTLPExportConfig: + type: object + description: OTLP/HTTP export settings. + properties: + enabled: + type: boolean + description: > + Whether captured telemetry is forwarded to the configured OTLP + destination. Off by default. Has no effect (export stays inactive) + when no export destination is configured. additionalProperties: false BrowserTelemetryCategoriesConfig: type: object @@ -2147,6 +2172,45 @@ components: truncated: type: boolean description: True if the data field was truncated due to size limits. + BrowserPageCrashedEventData: + type: object + additionalProperties: false + required: [target_id, target_type, url] + properties: + target_id: + type: string + description: CDP target identifier of the crashed page. + target_type: + $ref: "#/components/schemas/BrowserTargetType" + url: + type: string + description: URL the page was on when its renderer process crashed. + BrowserPageCrashedEvent: + type: object + description: > + A page's renderer process crashed (an "Aw, Snap!" failure) while the + browser process itself stayed alive. Reported on the crashed page's + session, with the session and target ids on `source.metadata`. Captured + only while the `page` category is enabled. + required: [ts, type, category, source] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: page_crashed + category: + type: string + const: page + source: + $ref: "#/components/schemas/BrowserEventSource" + data: + $ref: "#/components/schemas/BrowserPageCrashedEventData" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. BrowserPageLayoutShiftEventData: allOf: - $ref: "#/components/schemas/BrowserEventContext" @@ -3000,6 +3064,7 @@ components: - $ref: "#/components/schemas/BrowserPageDomContentLoadedEvent" - $ref: "#/components/schemas/BrowserPageLoadEvent" - $ref: "#/components/schemas/BrowserPageTabOpenedEvent" + - $ref: "#/components/schemas/BrowserPageCrashedEvent" - $ref: "#/components/schemas/BrowserPageLayoutShiftEvent" - $ref: "#/components/schemas/BrowserPageLcpEvent" - $ref: "#/components/schemas/BrowserPageLayoutSettledEvent" @@ -3033,6 +3098,7 @@ components: page_dom_content_loaded: "#/components/schemas/BrowserPageDomContentLoadedEvent" page_load: "#/components/schemas/BrowserPageLoadEvent" page_tab_opened: "#/components/schemas/BrowserPageTabOpenedEvent" + page_crashed: "#/components/schemas/BrowserPageCrashedEvent" page_layout_shift: "#/components/schemas/BrowserPageLayoutShiftEvent" page_lcp: "#/components/schemas/BrowserPageLcpEvent" page_layout_settled: "#/components/schemas/BrowserPageLayoutSettledEvent"