diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index b9b0a346..353d032f 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,6 +99,7 @@ type ApiService struct { eventStream *events.EventStream telemetrySession *telemetry.TelemetrySession cdpMonitor cdpMonitorController + otlpExport OTLPExporter monitorMu sync.Mutex lifecycleCtx context.Context lifecycleCancel context.CancelFunc @@ -105,6 +116,7 @@ func New( telemetrySession *telemetry.TelemetrySession, eventStream *events.EventStream, displayNum int, + otlpExport OTLPExporter, ) (*ApiService, error) { switch { case recordManager == nil: @@ -138,6 +150,7 @@ func New( eventStream: eventStream, telemetrySession: telemetrySession, cdpMonitor: mon, + otlpExport: otlpExport, lifecycleCtx: ctx, lifecycleCancel: cancel, }, nil @@ -367,6 +380,9 @@ func (s *ApiService) Shutdown(ctx context.Context) error { s.monitorMu.Lock() s.lifecycleCancel() s.cdpMonitor.Stop() + if s.otlpExport != nil { + _ = s.otlpExport.Stop(ctx) + } s.telemetrySession.Stop() s.monitorMu.Unlock() 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..98e83a50 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) { @@ -41,7 +45,7 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ if allDisabled { if wasActive { s.telemetrySession.Stop() - s.stopTelemetryState() + s.stopTelemetryState(ctx) } return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } @@ -58,10 +62,11 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ } if err := s.reconcileTelemetryState(cfg.Categories); err != nil { - s.rollbackTelemetry(wasActive, prev) + s.rollbackTelemetry(ctx, wasActive, prev) logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PutTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start telemetry"}}, nil } + s.reconcileExport(ctx, cfg.ExportOTLP) if wasActive { return oapi.PutTelemetry200JSONResponse(s.buildTelemetryResponse()), nil @@ -80,15 +85,15 @@ 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 { + if req.Body == 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() + s.stopTelemetryState(ctx) return oapi.PatchTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } @@ -96,10 +101,11 @@ func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetry // reconcile and roll back on collector-start failure. s.telemetrySession.UpdateConfig(cfg) if err := s.reconcileTelemetryState(cfg.Categories); err != nil { - s.rollbackTelemetry(true, prev) + s.rollbackTelemetry(ctx, true, prev) logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PatchTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to apply telemetry"}}, nil } + s.reconcileExport(ctx, cfg.ExportOTLP) return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } @@ -129,22 +135,50 @@ func (s *ApiService) reconcileTelemetryState(cats []oapi.TelemetryEventCategory) // A fresh session is torn down; an updated session is reverted to prev. Reverting // never requires a fallible collector start (the failed start left it stopped), // so the reconcile here cannot fail. -func (s *ApiService) rollbackTelemetry(wasActive bool, prev telemetry.TelemetryConfig) { +func (s *ApiService) rollbackTelemetry(ctx context.Context, wasActive bool, prev telemetry.TelemetryConfig) { if !wasActive { s.telemetrySession.Stop() - s.stopTelemetryState() + s.stopTelemetryState(ctx) return } s.telemetrySession.UpdateConfig(prev) _ = s.reconcileTelemetryState(prev.Categories) + s.reconcileExport(ctx, prev.ExportOTLP) +} + +// reconcileExport starts or stops the OTLP export sink to match the desired +// state. Export is best-effort: a failed start is logged, never surfaced as an +// error, so it cannot fail a telemetry apply. No-op when no export destination +// is configured. +func (s *ApiService) reconcileExport(ctx context.Context, enabled bool) { + 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. -func (s *ApiService) stopTelemetryState() { +// stopTelemetryState tears down the collector, export sink, and middleware +// after a session is cleared. +func (s *ApiService) stopTelemetryState(ctx context.Context) { if s.cdpMonitor.IsRunning() { s.cdpMonitor.Stop() } + s.reconcileExport(ctx, false) DisableTelemetryMiddleware() } @@ -203,11 +237,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 +254,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 +278,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 +304,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 +324,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 +355,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..29258bf5 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -358,7 +358,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 +427,90 @@ 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") + }) +} diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 0ebe40fd..f283c973 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,6 @@ func main() { } } - if otlpWriter != 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) - } - } } func mustFFmpeg() { diff --git a/server/cmd/config/config.go b/server/cmd/config/config.go index fe2240ff..eabba77a 100644 --- a/server/cmd/config/config.go +++ b/server/cmd/config/config.go @@ -60,6 +60,12 @@ 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). + 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 +107,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 +153,15 @@ func validate(config *Config) error { if config.DevToolsProxyAddr == "" { return fmt.Errorf("DEVTOOLS_PROXY_ADDR is required") } + if config.OTLPMaxQueueSize <= 0 { + return fmt.Errorf("BTEL_OTLP_MAX_QUEUE_SIZE must be greater than 0") + } + 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..0654e690 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,13 @@ 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: "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..94ee6c66 100644 --- a/server/lib/cdpmonitor/handlers.go +++ b/server/lib/cdpmonitor/handlers.go @@ -169,9 +169,34 @@ 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 a record with + // empty identity. + if !tracked { + m.log.Warn("cdpmonitor: target crashed on untracked session", "session", sessionID) + } + data, _ := json.Marshal(oapi.BrowserPageCrashedEventData{ + TargetId: info.targetID, + TargetType: oapi.BrowserTargetType(info.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..49cc38b9 100644 --- a/server/lib/cdpmonitor/handlers_test.go +++ b/server/lib/cdpmonitor/handlers_test.go @@ -334,6 +334,41 @@ 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"]) +} + 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/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..e5f9d8ba 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" @@ -44,31 +44,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 +176,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), @@ -262,3 +286,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..6da56b4b 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 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..ab35ca36 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 PATCH requests 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 PATCH requests 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,374 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+y9iXIjOXYo+isIPke0ZJOUqpexpyocL9RS1bTctehJqm57Rv1IKPOQxCgTyAGQlNgd", - "5bgfcb/wfskLnAPkQiK5aKnFryIcnmoxsZ4VZ/2jl6i8UBKkNb3nf/Q0mEJJA/gfP/L0HP5RgrEvtVba", - "/SlR0oK07p+8KDKRcCuUPPi7UdL9zSQzyLn71z9pmPSe9/6vg3r+A/rVHNBsHz586PdSMIkWhZuk99wt", - "yPyKvQ/93rGSk0wkH2v1sJxb+lRa0JJnH2npsBy7AD0HzfyH/d5bZV+pUqYfaR9vlWW4Xs/95j8nVLDJ", - "7FjlRWlBHyXu8wAot5M0Fe5PPDvTqgBthUOgCc8MLK9wxK7dVExNWOKnYxznM8wqBneQlBaYcZNLK3iW", - "LYa9fq9ozPtHzw9w/2zP/k6noCFlmTDWLbE685C9xH8IJZmxqjBMSWZnwCZCG8vA3YxbUFjIzaZ7bF+I", - "g1cu5CmNfNbv2UUBvec9rjVf4IVq+EcpNKS953+rzvBb9Z26/jsQ9v2o1a0BfVSIY55lL+ce4Es3KdlP", - "l5dnLOFZxmZcphmk7HqBh7kBLSEbiJxPwQx4IZhBxFq9yoRbmCq9cP8GWeZuaw7HtMoaWzNWCzl1W0u5", - "3Yheke2fuGEOpVSpE9hyAhx5QSM+9HtWl9JtN129i0tdAhMTPLvbIZsIyFJ2yw2rRrG0BIcIRvwOLBO5", - "sMZdhz/htVIZcIShjSAWboVZkYOxPC+YkOy9FHcsF4lWBhIlU5xtonTObe95T0j7p+/r6YW0MAUkafpL", - "fdu8ECMHw8h1L6GMNWHCfg236k63RKQTD8AdaPYM9ACxrOCLTPGUTZRm47DvMQM3r1nFrbTUyJ1GeeRG", - "f+VZNkgyldyw8J2jWAdBQmbtLjkXWSYa9+tPKMv8mm7TrUeLiAhevCtAHp2dsuqr0zQskjs2BCnTyvGb", - "PRhOh2xcaJWAMY5FjPtsbPkNXCQaQJqZsuP9xg5qitDEB6Pru5vzvzOROoY2EaDZRKu8g07D17lI0wxu", - "uYboosZyW0ZuFTlCEOKMvmKJSpuzVLi4hF6Ngyzda7VevwXTNRjn0O3C8uRmdYvHJ2fsvJSOlob4yaXm", - "CTANhQbjrkhO8W7+g8/5BY4jFmfct4xb/NGNRgYvCfuG7JWjeMNKA8ytIHnuJkqUdD+jENDczkAzO+OS", - "GclvYJRwgywBcQHnPZ5plQM7gfmlUplhZ1pZlaiM3QoNjKh7eCUjbDTLXmmewxZCCU8zwY/7zGGfzpWx", - "JIBaomdpCZWVuXxLmL+yyF9Bq8E1N5Ay+pARjbBbYWeCRFwmZBQP+r1JKVEcveU5rM7dgET40N0v9JnS", - "DPLCLhhhJjIGLpVc5Ko01ccmisJuN1ucxn0WOQt9HT8N/XaaxnGP/rtBjtHdlTpbHf7+/LU7sjt7YCN+", - "tonIYoS6RGGta27sk5ZrXUm/De8YqbXViyWmvcoJidmzjF9DhoDC7SNRWaRA4oHcLGTCEl4aiPO7guug", - "gGbZu0nv+d+2EuY1R/jw24qAwSlbm0FMwq3gX81w5TIbJLeWERU2mfELlc3hHEyZ2S51iiX0KTPuW8at", - "dajNNHCUE5w5QhXuClVpE5XDlsoUzfpQZarjHF/1qk69yl/8CME50nhnT6hjrQPQ7upWwL6WxhU7Ubf2", - "Fb4O97LECT2yz0GmSrMJz0W2GDp5l5YJaMOku/HMwbTQai5S0ANTQCImImGWmxvkgoYJaRWzM2GYAfuc", - "gXvIFloYYHOuBZfWOE6pIRBXorKMFwbCQBCazUEbJ1Ouy+QGLNubf8sO2Py7/T7jMmVcLhzXnzKpLEvU", - "HGUp8Sp3uSfKCaI31h+oz4qMC8neHZ/vM2GcWqG0w1Ju2Fg5BWBM8jugySwQqMODcGfzb9v/+Z1DilJL", - "Y0XmMGMKYN3bt9/DKePEvav2i1ohMR9jubaOqGI8Z0UHxkfryGl5qwshPjZAh9+iRugevhMuslJX6u/L", - "8/N356Pjo7PL45+ORu/fXrx7/cvRj69fjveH7OjaKWdukCkTpyTvpJdeLp+Djf004+d0Zs00uCtGVlsa", - "fp2B+wFf6kM29juNfS39ofYMABvXl+F2PXasRZW2HpeKFDGJxjdVCidQQH9j2C0Xll2X6RTskI35NZep", - "kpCOn/tPWMJlApl7b3sxWvApMMnnYoockd/yhdPgB7hmG9/8sR1PoyO5a6RN9vq9arEoSjm6i74zPJS5", - "MWLq7qSh3LB3Bf9HCX2nGU9KkvymLBxVMMdjzUDDBDTIBOIgvYVrIyyMZspExOZPipTa6hZuZ6DB3yeR", - "vJMWeBHp2vkLbmeRFxS3s+3nZ/9PCbrSRuEuyco0uuyKLtHglfd47aTFsZISEtttq4E7b+JLMuEIiUgu", - "KY1VOWh2cfJzn51lfHGrxXRm++ysLAqwAHrfPWLc3JAyYpn4wPkVri8U8stCq7sFmbGEYb+82drI4yZ1", - "+4uh2leFYlWhSIuRv7Wn1CPS4kSYZFd0SqsxkNb2hQ2Iws64oFcVfi3yHFLBLWQLVmhIIHVUNG6cexys", - "pcY9gYzVwPNHQbddNOGVC/qqBK/F2Ro1Pira3lPzrXe7pPy2TvL4RscaQbeyO+ZgDJ/CKFFljELp2e7m", - "diToP3baaMYXTkFAyRtZFwTaqFKh6W9xA4cGbmKP/F9ni+U5QToByMbEJkZJpoxTovAr4hxCCisQh+mP", - "yjjtrCyIukfJjMspKj9oGxNlzjSgfgop6ThgUHt3ujpKaeQyVmlgqbqVzKjmaokqs9S9BzyM+ZQLacio", - "J+GWhXWbW0CVbvy8+o2lwmmSOtwrK8q8ICWQzqqkhTs7qtQ0f+BgW/W/IwXXqtyeXRTCKXgL7yxhZlZa", - "d4T9tgbXvMpev7d8U80/4Z7QlrO0o82U2MTjZXSrMGAdQSppVAbo6us0eVzTt+5G3MdekVaaObZWTme2", - "aYWFuwQKQioyub7Mha3Fza1yQsgKmVhEeuIZhsRLKiaoZFrioGbGCzDDyg7s1z86Oz3mBAz/l6F/r/As", - "M/sOtdzr1LAM5pD1mbvTPuN6auipiKaiERqQ6rmrbV/OtMPHveps1S/NqWnOTEjoe0tq3x9lVOosso43", - "PLs3hffIuqeL19RoJOMaGMcHVNx4HJWX7vwPFpbLWPBVVnbLSrorT7RPKCqjMNnVnoojj4mv9D70l70F", - "jigiFJ9lFa1zPS1zNzNLFOiEXhd0VjNkZ+SMYUpmC/fmkh6VPbV3EW7Lf7H6fl2yWBN9RYxTLQ9Gy+Lf", - "eP/V/AjRC6l7640vcYW4nEU2E/cihFt0g9icZ+6FzbNbvjDsigwyV70H3WLUX7K6l9cN98inu6iaQXY4", - "TVacJczO0JWn4ba9x0fYWMscFRj11nb2yk3R7yFtrZo8ypzLgQaeIqcnCeVFEel0y2L1FpWeVJgi4wsm", - "7JC9UtWvJOL29knI9RvBKIFCmwTKPYnSFBFR1vdKGEzEHQor2p9XIPoMzQ5XvfdhJLKh5+xaqfyq50R/", - "47c9IZ1gzIWBfXa5KMB/fMeEF3iVj++qR5JtA890F7rKGn9bYY6v1XRrpSVTU9JIaq0hU9N+db9CTlT9", - "X7dcyz4Dmwz3h59AEoeDfZXDG+VwpqZPL4Vb8Pi8ZPBOonSNqOpUst0cfVZwY/Dxp1U5nbFSTkRm0cmC", - "7JYiIobesD5Gn4oqvTGypTL5Jzlz7zng6QvGs4yhf4QtS0zjVGXgmjkZNWQXQKYqU0BSuaYnZZYxhxNR", - "xvJEvP0VMt5l8KxCZ7NJmQDS34LltbBoZUf+I8/hwtMVia6O9wssMVdSWPeCk05UZJm71UGQnt5iwk6D", - "c4CEleV6CrZPESn0vvGeDHzqFSqZOeq+nQkfI0M7UUlSavfejjxocKqoo8JBGX9thkM1fDC0mbj+o3gK", - "unPWVCUEK/quMX+fOYUCXVfAk1njdNF1JJ+PDPxjdZU3SiqrpLcRCJm4Rzg6JuvrorjVJKhkffrM7QvS", - "agNWFQNEj+bI6CVswT29+aXzXoJ5phl+5imM1mlYi6L3QV9F5w+46SdqLLFnLCpH3tBVn9OEg3Jm+fX+", - "uhWDXNiCsi9xhNNQ1sbuaMhgziV5VmfCECq/IMeS+2CC0T0VTBwt4G9EOv3KglR9C/ZW6ZuGMXI9U2gA", - "q3mx7SPXKLhGfDVVgR2NrFrNQXKHpDlYjtqBh9zCYTMRureHaAbeyFNR/qrWBHFNLcQSNJzPyDkwfMp7", - "nLtk0xivt8m9KhMVXnUccW6ETLtUlXCgIZqSgzkzFurnxVjlRPHMdcjGFK454oUYP2c/43+wo7PTYC/c", - "c3xGz4Es1vTHwRQkaFS3ws7ZGO4sSIcI4+dMyL+T08bvp/ptyMaZSng28kGp4+fMLIyFnPk/MF1K6SDG", - "MyWnRqTQ2m7bZpkWvX6v3r/7KSzUc7y1sVBU0w2o0o1sESVlEz4EaUbI4LgV0cGBp5MDEhWnJy14B1pY", - "oi0E/hqK+cna4idwssF0H8LqcoVgMKZ2RiNZzgsH3VuuUwwqGQiPKW73jrWp0laxMyRk2C88K53Ko1H5", - "CTZm0vLYdWlZzhfsGhiXC/YfF+/eoorU0npWDoMJI5RCcJyJ5GbjY6nEF5P7NGgSvLCl0/LmgtdIiNyu", - "jq3c/DoS9UYe+kKKnunrO6nzndS4+hFC9glfS92weeQ3k4EMEqsiMcHHFxcs/MoKbmfBxo5nd/w1Q0Wr", - "Q6WYxoLl37xmlk9bAb1LszmAlUUBGmPFiVH9+P7y8t3bPjvqs5PTXzp0mKgy/4swAr0Djuv5VK6OhfvM", - "anTIR6e/i80NtxjVczdIlNKpkNy2T+XO4m6xEHeQmbglb7Fm4sX9J17Cw7ueW6lfQ5sgtPaZ1EDBn2Gx", - "keHdwOJacZ1+CewunOcrs9uK2d3A4uOwuhZcHpnRuUOsXODPsCBnQq19/uzxmO6WGNBLt8U++5EnN6bg", - "iXu1x7nQPbhp4Hton59h9EVSGrLDU8rSAjGm0GBMB3fantvi5Ou57enbs/eXfXb58j8vj85fdvPcZXUQ", - "HsBgLhKtsuwCrM0g3chqDH7NDH3uGU54N/GJrT8plBGN1FOMGBBy2v+82dPqbXxlVFsxKoL6yCPGx+FZ", - "HcB6ZO7l2NMoooTQ6uxuUGG6T9ijiPbaD+i+moJxSL+NWoLrLTrXWzz2et4ecw/+SWttUkdV7PJeYYS8", - "Wb1CZCFu8nCCwGq2OYmK3VtrqcWjLLWc60YYUoHOH9pvaPWG17Lm12IOTg3dEGXNMjEHNhdwW4ebLYVO", - "u3f8pMwC7/7GsF/h+vzyuLLhvIUbtT9kP/nvlMwWL9DXGRj6RGmcJQNjGGXufuwQ2Nh1fGXJnSzZYcXI", - "YcVHCN/uBM3ukbDBct8Kg105S3ck7DrPwOuKUFb9A0N20TLeV8Gaps+MYpxZzaVB8gr27+tMFCzhEokE", - "QwG9EbWKLceA8XG9pfFOxvItLnxz0Pwqd4gHzW/LIurg+RhUrhcrx/0ULOJrqPzuXOKjBMyvA9Cj84rP", - "KHD+vlzpha/YEaLmNVW5oBSVLq64o0duy3SvN+RlP2lwjw6ec+lzcBp3ZFXw9DiqyJSxQ3aJuqLVi8A2", - "vUMg1aooIGWltCILzv1RxY/d61JrMQczZJcauEUPgpCDQqupe56HEksYsWyB7Xl+PRJphpEfUxhlfKFK", - "G94o+4wbVkoNmUARQCvbGcjtGJjf40O5V9cNf2VfnewrYEdTpj0h+1oLoU38q41HXdks5/j3KlqhPhg6", - "1RIkolGVi1I5dCvvaPhl2PSDLo3afEObMy38VZxKYV9xkW1kBoG3USqMe1pcg8/CycTvtN+PTWlLm/9K", - "ZxvpzAFsNMEre3oyi4FnNyIzFopulMzBzhRms1d46OOZLBRkCqajepssxdsMDdij0qoja3ky28Imi5vY", - "fNrzIOC2IqeobG3RloYBYDySMLPKIgt3M14aS/ETWf3IIRsSVt8wQ/ZWsUmpqW7UspC+FVnmBXCVVOtp", - "+1OQcOzWvtLxRjquAP/RiLkTUE8iNluI7UtODOu/jjwdOAFKdOAwPBAAuwUNDD00ZVGFt/gSFpMyyxYo", - "ZpUORdvaBNmUvJEVH1H4nsODVfGlU0VYBl/WQV4SIwiWwbSs7mHKC4z3If3+uK2GY1kaAxbNKUvhhsGi", - "YjVPbtxsXlVhEw1mFowUwrBCCWk/KZ/5ymN25jEflb08hLUEWt3WKID1GJee/8zyG0Aqa6R7V/6FNilt", - "c78rvCG2yc33Uxf07DQUFqCFSkXCTPVtsHYEn+/cB8VsR4H1PI9EhEuH+EqDG2lwLQgemQRj0NmNAgsZ", - "iaD4kRv40/cDkIlKIWVnb/+yJYJW13a9sLBRS3drrznjW5JQp2kGGyMjgjQTaYjcXoqL4OyHw8PcsH+U", - "AqynO7KpS8WEHEwyMZ1Z5svaYvD9lt42v/RD6W3JD/6VwlYprGlUfELa8nj3WvFUyOnap+EqAmY0Krxi", - "fQGL00mrLoi7bZ5p4OnC3Y/HPYx8cpojx2euewNLxQotlGbjcHY/xRjnaHqKhd3vs3Gps3GfjUNelPt3", - "lc40ppyrsQafRe0uYNwoGfGCjSPIiJl4BddUI58VqigzxBJMIuKWJdzAttUmHolYOkH0VT5tpB6PoU//", - "Cl0PpEeOE6KCN5tg1iTAMGI5tRHDbKaRws8N0FHtx3jo9duQqoWpqo3fvElLgn3+/OX5+ej43du3L48v", - "T9+9HZ2/fPX+4uXJ7nXfHbuI1H1HD1Z4IiotpkJytEAtsZFO55VbtcEl4gv7kw7P/aeXiwIa5gBcYSXt", - "t5nJ4jN+f5bqVlI4qmFCYi1FduLTLPvsFdhk1mf/+dN5n1GFoD67sIsMzAzc2/Y0x3oDbyAVvM9eKTfm", - "Eu7spXvZ9lmDuvt1jbo+e8OlmOAOzzRMaI13dgaa2GSu9BaFtlul7BtY0a8Rcm28kb/C0P1mWykTwIfl", - "KzqS5Z6e/TZ3/ZXxbmS8HmhPz3FX4PLIvDZkQG8sw1KlSqOe0K7/5m8jyntmjey5XfbdzLxbLf7uryVk", - "2A3dSn5Pjmw72dxp+GaINXiETLEZEmawovpTmvaZ7s3zjOduBdfG8aFCg5PWxJCwwEH0uoQZaaBKfuso", - "B62BXlQYv19TZtS/iIUZ4iRDfpuONiDeqcMNC5Wb3eTYyIJE3l9eXvbZ2buLy45C/8rYUWA/cZhdq3SB", - "osXNcnD2/rJ6pPXd4fici4xfZ9AhyuhocXx9R+Ixw1zra5goX8wojEIw4MFQQW9cNl6jLuGRpHaflVL8", - "o4RW94nazfNVQj9cQns07rdZWM1wVhjCdsKbuuDsIL192xwNCYh5/Ux85TbdMF1WHyL6O6B4nwEN66Pf", - "EbEyZA2Tl/DTKAONW/iqDWyhDdB9fQx1YBkyj6wPOOyMAslDooXGNTvFsmsTX9KMvTl985JK9nxUlcDv", - "rKkTbCPrvIKjguxYp83kIu/i0dWhw4TVVZHgdDdzMLN51mfLTRi/vhU/e0n0SN3TwjQd9oboXI1qF+9+", - "7rOq3eb+fQVm1akgEOJayXjGp3Ci8mNKPH+teLqFCfXk3ZvWgFBrz6GPm3CYVjPiXCgtt6ytV/Dpgwvr", - "dR7qq7TrlHYY+ZuqfORrEKD18Umtjuuh9NhWx7QYVfcWYXwU9JGHel6MfNiUeC0kC/5rbn0xpBUSmLj7", - "6GM5dyvmCOJALiH8lKI29pwqiFDDQmr7Q/beABtbQwWObtse9EjA/HKjktbJNhL7awzu3jaPmULBO/KY", - "n/lr8Xow2mIx1aD21lnQc8CKRGGmmZjgU7B+m8+FKTk2c7wWmbCLIXvJk1lrAAXH0FP42cCv6g6tPx5T", - "+er2246HtLMHnph/eGx2OLK5OGyZl544W7i1d/z6Yt+jdpXxdQYaL0AmwC5FDthz8ujs9OMKseXjfZVf", - "2+Geu7CPjHlPYr71UUyrF3mylHHVQmiQVi9WQq/2fC3yQxQzLXbMCtBYaXU/mp/VvNVRCpaLzOyekBbI", - "qXFxjFurxXVpwWygPDzSKu3NeDrSkDh1RciitOtRunVJvmBJAik5FrEaGk4SrHoYhtL3LcOcoBKePxy/", - "voijPKoLkRy25romUTq8p4TxsNrD/uruJkIQ6uuL/bjoX8FJ/6DbscBqKLaCf6/rwreuqKrnGq1XIGJ9", - "gaPAq+k9hq2bMwSXUwaWDuz3UufqbaEEJcVGcfGa66l7THs1b1Jm7IwL98x5fXz2ucoLf66vcmKDnEiK", - "pxYPTUg8sljIkuKebNjjdI3ShNEPZcO+rkmU+4i0nj7Q/+vjs7qmnZgEO2NnjedRnNm4l1fV9n1p3q0S", - "j6VKu1nmybs3zH0Q4ZqNdbq6cckUdMe2z/HHbTf+wgtsasxJVj9fY6TKvrgUuZDTwVGWqdsBecniidbi", - "d+iuQMg18I4NUYkXZv5R8rY8qOfe5GFuzohRcO4ITGk2Fymo8FNHweSnFXrNrTkeRtB7ArmHC8WUs3sL", - "vc2STvHNr/z65b5syMvC8E9hwqv2/lWcbRBnij/5Q7sFi8/cOIc6Zo3OX4pp7m2V97UdxTabDPjui8v0", - "i/zibehCvT9kx1xrAVh+v6q1PaF2dUIi17rGatWW+YrzvoNRqIzftMQt94T4uNxh6ba+8oj1PKIG1hNz", - "ihhcdst0uZ9Ur3ut4xe7Ngx5C7dsfdMQVvVSr17vG/qGFFw7tbj7PGf4weqRqCP7Nf290SnjhY//px1E", - "eoZ0tODfuSHIo7X9+LjdPGocsOrRWm9Q4FFD86qxaGtSWO9vCV1TMUCowxFXdR5ZMrCzGZ8Du1Z2RnKu", - "ctWbNu60XC5V63xhWGN68sRgJwIM0WOnMoXCacNUk7yZ1vOCcWaEnGbA3BeUl0zhB6kC6gV3jbJSbN3h", - "9Kub5lPIg4/kqrnk1+8KkGucjhJuKwXH8mv3OPT8xF2rwsGk2/hiIyH96lLRHxD3Ea9pnNmnSD0TokV5", - "q9qOMHUCl68F6rYQul8Z1SrXtylZy+tL7TSthuJUUQWin9MrYylcQ3aspClz0O4dShlqS3oato8JLUNm", - "WNXEYqkvYZ2uxtGSL3i2U7rXY2llbSh/VcrWE6Hl1yPC649KfPfQyXCXcc3pcqXVl9fIHA1jPoEnXSQG", - "JYECweViVyWj7gYUa10m4TZbVEvx6yfRPKywWcT8Q4kHmec97ptKK0WGEt9MVI0JUzVMZ91zLKPIWh1m", - "DYpcgJ6LBI41N7M1DDrnkk8hxaqnIgEGd8JiLUK4K7C6RLZwOoPTWhCrfE17im5z2MSt0oM6u6Tqps9S", - "hazRd+Rqss3/87/+N8Wf1qvguga5rgWd+7BOfAIPpmIOg7LwBWmpvVyqtuWC1E3roXwwcptfGWEnI/TI", - "NEroup6QEXbBZffaqrjXdmXVpWNURVXZyzthMWIUEda9IHmGOgJWEL9zqkCV2VrKFHSGPfeCZYpoTlfl", - "xJIZlxIyVE+QLij5hAiS2KJdUMfnTEwgWSROQ59xQ7Zu2nnw7DIhSXvZw6dHlZyzTy6g0xPcqIZCYdXE", - "CBXhzLGyrVssPWRjJNqyGLMcuDS+TzUdPBXuXkhvExjTrJ1GhNoaZzPgmZ0tqoZ3WEZpyMb+v8OEnBUa", - "5kKVJltUY1ortJnXeMrnMIpvKECiKlbFHBcKxZiq+lgIZUtVWq12sHzBZF0zrgtRqHace8LV5oUAViq5", - "alQOdtYoAGWqp1VFS3SdvX7P30Ov3/MnijK1ImqUOD2pCvnSHsMVDNnRdZ1fFbsbtxgri9WCetFrwpbh", - "LFPSDa3KW3Gqyn12etIRY+0vUPKoJ0arqeZ5u4GXP0a4T99pEit/ijJ36nxeWgva/Ys6Ig7IxTbghRhv", - "U8awuae+p4p1rAgFzTuV/yyybE15stdClneMtsTevXszuBFZhpUHUe5h1ZQKCEJWHR9/eTNkF80W+eOD", - "FOYHN7mZjsObyKEZlzU54NQVK/JreqGRQ670ogIomRNCXIy3z3u3GVqu/JxYmpnbit2ZsnAXZeK85Akl", - "8sp1fxXI3QIZL2ukVD5yKPGUAjkOlt3lsdvnkjhuH6K7xnmipLGaixgF/jpr0wIkIiVbQSDFIRtLJSGI", - "i2mmrnm2Si0v2DiHPGmIpWSqVVmELxH6iB0zYV+wcVKUBuyYHeA4pRejQmUiWZBx4e37N0cH9IdBqsUc", - "JNJuzZ6V9Fs2TGUYazDjkv0wPPT+sVSkVf8S3xpHlwl1mxorlePRno9ZJiS0BYw7LCab5ImTLbRP+kO9", - "y45usfloogFGN9eR3jMaILSy9VciJPtZ/Bh69zSDJdzm+iwFjQmZVcDK2M3+/O3Yk5qQDdB9Y9gbyAen", - "cqJYWubFkB0ZU+bgIPE9rkOFRMTvMGQnwVATkpY0JBkXOVY/T5wCErpemJxnmfdDYsw7Z5l7diHURlZZ", - "no1ursdYu91Yh6MO/HTjdFgHcrcUKn5sxnVKXdSwIqeHpmcjAQmbsOOUgI47qw5ofAG9ZrfYBrk3txZh", - "XO6XB4PiLd6nYedHbwiLHgCOp7mFTZqPF4ZB8YnPQT92KCLHKs/jszGMMSGdeknc7uX8jj37wWn52vQb", - "sqL1WUdGoTFRkJ6DwXcBM2BJ2MR35cG8Z0rcN5dKDrQxfTYRGdC/ULed5ZC7/9wfskunpfoSBcVsYURS", - "c7+meujQvMTG+B1I1NWoqhhZbm5MDE8LVisZ11h0Fk85MGAHeEq/VK7yRktVwlhDd++mJN/FkrbUQtXx", - "pdsCvTDGzHtGXuaFXaxDSm8Ac98ec3wMcMt+wPAf4dQixa5ViS4dklqI7IiswgKV1txVsXH7RArnd6c0", - "xw/VrXKt+YKUFjGdgh5tIgD/XeMpug0p+j5rMnWcbHx89v45e+s0efc/jiCej30Cb0O2ROAe9rg1gVWI", - "NlMGGM8yRQm4lU+qUfvD79sqJuRc3ZDCXOvWQ/ZuYv3zBn1o3LBxcydjtteYxhNRIzkW9D4GUSRcslRM", - "JqCbrTJxUELb9D+7O52LxIp8yN5sQ/+te+sq2di8O+J3FYvYViVDhNpNGzuqnIIeIhTvtomqUAqs6Gbb", - "w/0hfHMTJayVAdsz3bYUXeVKWzTQIyB6iG6GZcN0vc6W3sxqJwO2e7J5UyxhdpVQ3nIX9XvXPLlxiqxM", - "R/4v4SF8q/QNaPeHGdeQ1v+NxXGiGmLYdSiqf0xPCQHmWMmJmN7HTudfI41K/b6tKqY0Yrd991wIXsfO", - "RwIvbDLbPe5t+SwLf5LV+gbHtAIzKptDsJIwVdpE5UDVDhqdwJ5wH9QGjfyiBylYTMWsrHnB2eLQp9Dq", - "boEqQdVFLezTKPKSPNUmaQV3PUVp2V6mpn12y7XsUy2/fdyVYwHldGYZ3CVQ+OgY2p/VKnvC/R1NnSLi", - "n2bUg9UwPuVCGtusYNhnpkxmTsAISdpAwrPMBGf0iiXKt5vz9bDqvr5Pd5L3lBVVLRWMlXvY5N302Q0s", - "UnUr3YsIO4fu4+ZCwZqn21iz/O9BVVMhB8tTbvmQoq+mT4mEZxg0XVFGuJhmxA2VBFvKfH59fEaX1Cg/", - "/YS7DHXLr9uVokMt6Ea5aDNkP4npjM1VVubwgqnJxInPFCa8zCxVNivsQEjaPRnmnm7nIaTslzfecF9T", - "iyrtQE0G/qGBtg+q7kH+yUGw85Ld11H9hzVSsmNHu0kbGhSyMtG0GjShpuyhFSIZNNI9FdKYCQqo1Qz6", - "foI4w39Te/AhO5WsWWbN9yD2AS4EsedM5cL60C9hvIlkzzPz25lCywZNvs8y4PPQVC+sqCYTb/Rwa/nF", - "DYM7nljvhEoqeY1Kj2NiugTa39Hl8U+NQnBduzE+kgzbhroHFkGLjf/4MN7HeB0m1UAVL9qb02Adb0V/", - "DHqXnN5FDqFLxehumdIsFYa6mNZD54LT7vpsoUqWl1SrM8Ut3BWZSIRlY3eQsZthjMAftxTwylK7FZLd", - "B7nqZoBJBM28MBmyi1XAD9m78CwLOs4NLKq7Xr7ofQe1oCGh7doTvwH7nGHx61vAEtjkzfdefvRpGp/F", - "qrJ+o6NK39uSqJ99MuP7Q/Yr5deO/Y7G/dqR2cAhBw6HR540niM2of0zoP4LxuWCPGLKR1C6g08mQ1a1", - "dqnn2/N6ST+EMVK3w35TvJHiMK55IhlcI5wQEeUa3DWjL9OqITuqj+QBFSph0Cb9SViSAddEXzYOWTrA", - "2DcqGNfoukf9ajLfZZwY4z450mw9h0PyGWh4gTnLmbo1jJdW5dz6uDH3IEWPKm9eU5uxRDw0/njbhs50", - "avJRUjn27rjASuGl0+V2JJVX1M6IHpzX2B40ZcHRV10QYFiJt+8abxUnTzUV217l0DkY4xWK1Rdk3Ale", - "rTaiqXNeGOp9gr7Qg4nIgKjDW/MP4M6CNELJg0Ir9/NBKkyR8QVzCPuiim/2E2KdUMf6fNivAwW3wpdR", - "afY4bO8EH5HNmaLvsXh/uHeFp/r6Lpc7wg1bHmlVjML9U3UqbZt/8O0X8QLcVfd71SW4//DnDx+KMh9N", - "Mj41BB93RZv9U+HMAYSxJ/Ox02ffqNKAL2S6Y3zcdWltrFADTsnoVzI5EbNBztC4pwwm1j3qHXN1WxVp", - "moUXNrnxbrlOo3BCVbyjoNulf9vjN/4p0ljVqe69fg8jBvCT6AIzlaWjG1iY2PFSisJzP7vzuW+bXa9o", - "1oZRczUkb8lAKct8RK8LWg55bu/5s2VKf4s5V2i4EDl4wirA21fCuqsWm7vVU/wnS5TSKTrUq5gBvLFC", - "URBZdKZIKcX/us9MS+h613NTdyBpca24Tn0C9444Gq9Kd+wVpiRMTiXpfPbB5phDN2l0s9ThTB/Vb9R7", - "mBF9u2LtcddBGe4gKS3aZQuufVFkZPVebFK3DNIqSRn2LP5K1rMUlLFLPo3V/tE02l0CapbuAz+24Jrn", - "YEGb4ZV86fVfJavfaWSrViNa2ILGUGg1F2lHBASScu54xiYZu8qwPvR7qebT7YafaD5dHp2rOWw3+o2a", - "w/Jo9Fs6NrFp8Jn78GdYNMaS7WDTwAv8qjkM7CgptVEbNZILsMf4YXN0BiTg1g50H3kUbsRKrEbqBCvq", - "Coa15HADvq37pplDR4L6KquracG2dfJwkBjnrifdcEwnJy7hzlbXs0zl8RLL/d6xBm7hBKtsK724n/DM", - "VQprNI00zM7ch2xPJeijxlP2GcZy/esPP+wP2QkJC5QF//rDD6jEcWtBu+n+378dDv71tz++63//4Z/i", - "yXp2Fgl6vjYqc9ym3oT7EN9JePSlRQ6G/7zZNeNWil3mCWRg4Yzb2f3uccMRwsZTXObxN34OCcq+6f12", - "H3PDnK4kFeiwSOMk7CgrZlyWOWiRuGf6bFGE/vQN+PPB70eDvx4O/jz47V/+abs6Eyekfm75al8qToUv", - "5W6BG1R7+q4us9FRUQS7fY40t7B5Sv8109hbVLKffmd7OV848SPLLGNigu/FFCwk6KTejy56K9IYQi2v", - "hp+t3X/0apcl0NMo3I5tdijblZJNWnc0xhDc46Ophx4uqyon7pOVamvXYG8BZNiIU7R9ZDDX1mOv4/+M", - "Z6pKyLSYQp8LKXK30cMYTNa23/SpOBjew8KXK3sLThxHWhrohtxe8iq01+RK2dm/k/0OLUhoagomBKdx", - "uzNccwMpRqbjgshfMpBTfw5+R+d4dnh4eNg41w/Rgz3kleGOsNMjI84p32ms+8IyYVCt/Ntdny1+a6r0", - "BRfaVLAL1a9vZyKjTUwxluSNU/W87si4ZRlwY9m31KAX3YvVTpe33AzUqsI4vsXLq/9j+TRrfyRYtnDY", - "wTXi6WGzMudykIkbYD/C7wJrZuo51NiMEL7lCzoIE9JY4FhjPRMSuHcVFSrzVqxfMe7BrYZGAjMqQI8M", - "TBHTiBygGCGRjXKDtjYxlapde6cRCdv6vHWkH3aky6oYCO5rBYKntItVathInyvnbL9iD7ufsdWWELdo", - "X1iY0d+XD6FDNtG9QfaGtseetfb6bHNwQZdwr8xw2xrEliZeZ3Z5SW+5s4wvbpELbysM4s1nGq/DekpM", - "vonE/aYd9hKqRn/wH3zO6Z+UvVPPTc9M/OOMG8axSbj7/ZuCT+GbPvvGZ+x+Q6/Lb7zZ9Bs251o4ceuf", - "jnmRwXN21eO3XFi0Rg+nyqq9b2bWFub5wQHQN8NE5d/sv2AabKkla3yOuYZ7+y+uerGQICoSRcUCkhYe", - "/mkFD98Qt/ZnxCeMb+AcosmDes2EYX86bHH471r8fTOu4eVviQ8GN7wjOoRuSUtYUJ9u1QMXsHwpzh57", - "A3oUdnpTfT++LWO8y4Lf9Oo7kaKFCZJ1fBJubo/SYveJjaSgI/u5CMF11LSwiqtqHixiyU1VrDhqNZkP", - "pNhyNuqKv85XCc3bhrTVSD/uOGsl0/gFYgjySmRwKidqlR8JM0qFXr8rlF/oRqyecx1dtFRn0UEnynNU", - "SHyIYagFVaVapNzCwNckXY2Dj/Iddyx63V4L6zNm++yql+rbOz1w/3fVcw+bq95A3w70wP3fVS8ezxaP", - "mvuRG2glRU1EcIqu3sTWr+Kgs64iifgdRtcLCxE8ufDhcPjz0Nc3DNsQYLaIhAtRjRz1+sZi/YAHDRj6", - "S+9CJwp57EjCelX5aDDzcgpdTRy3QT8+mVAC89Z4eF9YVkvdF6i7YUncLOZzlBYFNG1gx+cvjy5f9vq9", - "X89P8X9PXr5+if84f/n26M3LLfKNKNWoU2HBzjPLPsgO+J4I918hl66UvqZ2VYak8s/6+J7Q+8Dz7Z8p", - "nhdDtOpweF4l1PCMWX6npMoXzzHZjpLafeO9enZjNfDchy+PU245uZCVzlGzULKCNeoQbivXkKlbtkcW", - "btoSmb59pMS4+x7GfaZhynWaOc1FTdzCrCivM4F5ksIO2THPMtCD+o/+AjBg4t3FJTuodn/gfwpZflVK", - "VairIgzd7AtmANh4aS/VexT7EJoZL2DIfuGZSKsS5wluJsTKN2PphKkuOCQiJL6AyjcmNNsJHlHUkdIa", - "4iTwc14Ugprr80KM3FobHNtHhXDXQyjVD9GhI4zdHAXhv3YGH+554UaQtlJNlhYjHz6xaY60OKYPm2Pd", - "8bYdflJ9W81AMRIjrw2tn4C+RQ1peXymptuNfq2mYWwjDoMcgBtmOK2/R2dIbB50R2w7y8+wiM1BFviq", - "StLW05G7olX5q9/LxBxGcwG3WwL5tZjDLwJulyBdT7M1vMNMq0D3YSaNqTYe8w0NOWmMWJ5NSGFD6/Kt", - "JjuVwjZ7+NdTafCr7DTfeRi1YdKd51udqxnKuc1UF9X3YaZmabXt2jiephksj17qGH/P1vyNCUMn5J27", - "TLfm8P0Td+9O2et39qW6ZwewMONSl5qtW7G0qXm16cjuPV2qaZJihwr/1SjF011KKYdxjXKgO5daXZ1j", - "h3vsqInYXymItWutsUbySSgls3OZnl5/Jft918ICPjHUvQwWb1F7JwX1Q7+nJGwfKL0sHz/0dxnWEMpb", - "DozR8K5Dm5S729gIE9ptgpobbjkuhtc7DI0zlx0mqClyh0FLGL/LcstcZ5exgefsvl6TxO8FmPvMEFcM", - "dx9c6YO7D43ofltO0qEh7DZ6VS/bbfyKqnPP4feg5w5lcMvRrZfZtixz6R21/bBlVXrLkVGdfsex91y6", - "69255fCouLtvTTyqf/9aGItGtohBSmu+cM//VfOWkGRtxZQ0Sqkfbps6X5mQI37hStxGqh9marqczdxo", - "9bw2Yny5b8208ihYuLOdfUY6+iFcitx366p2RN3MKGN3W1t0h5uuuXTMuoYBFmc+mvW80u2XzfHbhtmG", - "ILb7h9d2zbB1WO1KNONukSiPGJGB4X0PjMVIhbFcJtBy0P3w1BEYbs87RWA8PCzBW9HrGAT3Ty7t0i3G", - "Deub0LMO8QgYxqy6F5puO9NO6Hr/GMEUjB1tinUEY7G3vJKVh2dTqGC/Z3SyaWIqCbb1nMt+wbBAv3GK", - "2A29u2nypR0cx3+h0trs3c9Vn/ZVvq5uNmLtKZXaBxM8n8PNXk91Ez3LGbfJzIch3g/iXXGIJ93xhxWj", - "+Pb7w92jEU86oxCH7HQScvX6rDQ+y3QmpjMwtq4/SkMCV9SA6OOFrPcj/emw/91h/9sf+s8Of4tvEa/W", - "G9Q2wWvio5Q0TBzvoCQt8TsQC67qGzglpApAPdCAxxQGg77nEOc0PturznlaDXKtV6cimSETzpfRrM8f", - "fJBWMZCmpMqoPOUFxTxLuA01zOpQDcQJvMsZ8HRSZn3KpAx/yTrQszP886Qz7LNCm+++PdwuCHQ5F+B+", - "kndDgGaQukFsUW2FhaGozOVWbA0UdeA+7NO3XAOzWMhpcwzYGkFaBbXnmyTqDSyoFhwz7nK8RN9ewMbX", - "f+1DG93sZpFfKyo3gQv5xutuidBY4BoYb3zLTFnUdcvuUmWVyq7kngFg//nsGZ5lkbMUJljkW0mzP2Q+", - "0MlU9fSueucY/nLV67OrHtok6J/HVmf0r6PM/+nVD1e94RWFN1IEnDAUn5ngBnlmlNtlovJrL7KMzwmg", - "+f7FhsgJ/C9c7V8u+TVOu8OFLnFrvN0ov6ZqQS/vIHm0WDbujpdjvORCOj4isaDxqmjietoOi/xbpHYK", - "zcT1tKw6Lm6PVdyMtFLtoMb4Mcp2hWDMhHZDWaHFXGQwhQ62w82o9EnG66cMjcnc124qWWYoPQKPX82U", - "pLNHIhXwokOZADODLKuu3MmCMt7fKbmNVWxQGssY14/VPd6MrNj3M3pfNS1CDTuXD7BZ5wI570avP2Lx", - "7B5mf3xYBthLORdaSXx4VHGKWJ/WN1aJ18KqMX8l1nC38MJuAHZHERI4N5Lhg0IIeZPoKoBV51glwrXv", - "wZfV+bseg/E6Y3An7Cges3oWKq2FQvMdZbMxonB0/afv4wFFjeIw9Cm7LieTjg5eFFG47WSqtN2TfeiG", - "3s+iTvfbDXwXVGYfsVdW3X0a2NsGGVWwaDG13uXL8ze99fM2w5r85z+fvn7d6/dO3172+r2f3p9tjmby", - "a69B4nNURe8rTagWJju7/K/BNU9u2kVNl2OiMxPvjFf12UhUVubUZm5dvG+/p9XtprncJzsGqeOsfdro", - "mhu7KPitbF7YViWKIqJ7tVepry0JI2sXm6Xgkf+acVYYKFM1qE6/d3b5X/vLjJU0exREVQjKHEgidYjL", - "ONBCG5llwNGDpnkIDJtaTm3YAaQrK7nP7r/Mh2iX1DZc78HPTxsGY37tGBJnxs22jh6ilSHfXVTA6upQ", - "EGpvxoZfYAm3QdVLMtLJqLGfyo5bliKNM2Js/DriNm4npurwK/0a/LAdTMWdpGa5LXftQ3/cKNJUGpKy", - "3VypKEdFEmubaKzIMW7z+Ow9K9GeXoBOQFo+hWiX8jVitO7TItq1RWfc+E5H2+goVGC7I/K53nEoVxyq", - "JdPuq6DoDgkeNbec1TC1rUjbugcIbT8ui7oBmwp5P6Fzwi13nOxWCzKALqEeJR0IWZSRQOqUW76VYpE2", - "V9ncoqOa97eNZ36Qvui24xM8jZtu9YTuCwuyC0nqjDD8gPnPh71tTSr+KBp4HdW+i+508bKqSq2h0GAc", - "h2q0JPLZIkqvlD98KDQrx1qNLO4UURUU4n661+0trYSfO1KIpvpuxRoqRkqTC8OucOBVr4tk3f4jUoAM", - "4T7sWzUahSSzUt60Kyhh8k6VErQlEVPcNsL/YXaIa5UuqLEmTRnq89EFSE/dy6Hs63uBx/IEquKIrLKR", - "oZ0inQuj9OK5r3Z3I9VtWN1XegkdsKoWzUu1C3lpZ0oLi9mVGZWspTRT0yhAOGSnCFBqNmd8Sa1S0oJJ", - "aazDzUUBpu/QgGyvWIGLeEy7UUYoglvXQu2HosnNyq11NdpWfd+qvGarSmgVaV5Hnq5tjNNVRJAuz5P4", - "8MFdcDbkfqxvBr1tnRGqLQE6nvs1ERKTFLZRg+oCEmFUlxK00Z5E+t3qn01VCaPxeyuNeWulrd6tH3TP", - "zS7dMyqTzX3G7ryOQTqH6TY1nLbzO/3kK0iGeh5TbwRZU/2iwxPxK3ogdploy6gEmusb43usTxz31xIe", - "FKeww5xRV3C4hX642E0gu49HRVeA3lCIqY0YURHULte0q5c6s3x0t96x85PS4nclsRgQrsV4rkpph4zC", - "U9zDGf9uGKYA95mEKW/93cEhLrlpBxtqf/zidpxssX6qbmVk+bKIL/6QSIyqYNT2Rv1NVMGtL5FZV7Vq", - "L7U7Uew85dbhESulvnbkWiJNQW5IbqYwjtpH5gdt9PH77zq2/UpkcAY6F9hz3Nxv/9hXLG54o5ZjlDeq", - "2V9a1otdE5QjNbj+9P33+7uV3FK3MubncXvFn9CzE/b7vmO/2ySzUl5lUd8tuXPJc4gu9fS+5bDWJBc3", - "a8ft2LuClwaapQaonngBiaP9tPId7Oh8aHrCsWhczPfQLOrQCho73EiUzcWjF+JUmFfmV26TR61wVpWf", - "Q3MAVoKMl2VwhCvmsNluW1G7n49VY7PFFrE8nZFJeAMPrJM20TyHeOTNea3bho8ciCeFo9g5aC1SMOGt", - "5G9gvwnzbw83GYGjJtHwYIsYMxsKLCDtPVK1Ntx0QOhTeUEI3O14rPfRdLyFAMz1t7P2QnJ+h1UExO9w", - "Kt/82L0DjGIOrYDe/LglRJaLZz3rCMhypzsqU6E2I/exL/DO3edUgAwbx85FCmrIzgmRTfNd7fQMPgf3", - "8KdRPpLPPaXPyszAkf9rcgN1HXJIqVUWpsczA9awa2VnjVrq+75GFAUptfFFGNrRwL3Ih1vW27+wqngo", - "fSmdgJtn802e5jmkglvIFtjzGaMcVGnZVPMEJmVWtbj3xQFyDItDU6GQGNehdYkl5PGoiCNxN88u1REJ", - "YG5DT1gasc7il3PIVLFrrOUlVqCjoaxyh1jsq9koF8OWKhBEemcEQ+DaAqftOhBYPPYfnbb0Qa6kskqK", - "pAq+YuREqHfKE62MqRqtNzsa+jY97L3xfUFfc2MHuPLg9MRHF5Y+iP/i4mWwA3rzpzBUKY4sSisdfHdw", - "l7ozBkvpb2th2JX0sFQAg0pf3QoNgwzmkHlbEhZtwEJYRaM4hoccA5nieZBbhAIavgRGffohO9LXwmqu", - "Qx0Lr15SbxdfFKMuAeEYWEqTDdmrlSZe6yp19GMlNnDHoAdosyK0YalKMEgKqn6yY28E+2dfu+Jg6S8n", - "OG8jAK7PVgt0bGp+vdY8+lkbGWsQ/sfFu7eVjTEGn0wYf6/rC5VQ3SZyRyzDq12zOwYJAqS78Kdr9n0B", - "NmCZF5eVm6Cz97d1vN73y6z6f2/f/ht7fbe6f7caf7cqIfsXqw4Nw2l3PsR1xx7hT2vkrWB/ETyd9/Ap", - "d7VUWQ2WLIpMdFhhf+VZNkgyldzQldXmisZltpvxOPj6KSlPx4byjPWOlvq0bO+A7/sWHjt3XvH9Vu4t", - "7rxEy7ixK5K47pStwYANErF9LfS8jvei3+Fp6Y9P54jizlIF853tjQ+r83sDC2O1ugETrc0ZjX6J1w+9", - "V15UCNis9xHywhr5UY4T3WHb44wvhleyxSR0CWwv9LvNQ0bcQRqqNO8P2QV19aoSCq6kjwB3LMCtRT3Q", - "JVPhPdhYr3VTbA//9u+H7l582tb+8Eo26sViEwp3a4uCpMSt0ik2dk7JR+pDiquTC2k1H7ivaEFzJZ3e", - "IDmV4UKBSD8XvDQOTpfY4MrtzbeMN6H6bBR00Z5f/Y6uGg4V8V6xLQAJg5nCsHVqaNFRRk2NHMEksB4X", - "sbfpjDsB73T9RaGYkH/3zb/cq/8Fy4Wx/AZIUUI5iToI3tk1T25MwROokYAdDtk7mS08CzOxG2B7RmQg", - "bbZo3dOVrD9D3Ninq6qesIfDZ1GsD2E523YU+VULC1UPlPsR+npotQJWQtm/sOB9W6F8wG6d5LbEeq29", - "5z2vjZ5ia092dHba6/fmoA1t53D4bHiIBtICJC9E73nvu+Hh8Dtf9A4PchDyiQ6oHxIZx5KIdewN6Clg", - "bhB+SSgAd8JgUIeSYPqsLJzwYUuTRjKS5sI9zwrQ6JVP+0RkWJC2lFZk1B02fH0C80ulMsOueqjuSSGn", - "Vz3MW86ExAZW6hp1ppRdw0TpUBkV7UU+dQ6Rqeo3d5qifdQms7DKK98Pytcq+lGlCwpmrXvk1GnaB383", - "ZI0liRlxJYfbXNIuwpHoDq1iOV6rr9T5t6veYHAjlLmhtJXBwPcaHEyL8qr32/79M01oQ3G0qr9z9EnJ", - "Zpi1iOt8e3gYMeTj/gneKb6sqqN5YC/Xa/3Q731PM8U0j2rFgx95oEmqGP2h3/thm3FYNkPyzI/CCrN5", - "zt1TqPee8LLaYsZLmcw8ENzm/Z5xWI29VTexTVRRGtCD0JGnXgawjLkWBhh1ZmO1ra4Kebnm1c9Dh1X9", - "K7mRXNju1HIldyWXY9BYeT7cQuiL6h4pvgG9kBPNQ5FKj8XsZWi8duF7Y/evJDZ5HmBpckirGekc1fwB", - "DdHoe3xydhCy07GjogZ27TRpSK8kWjjCXW6k7LO6Kdx9iTsuGmIa1TbAH7KfQy6g/0nyHMyV3PMZZ16a", - "Hit1I8D4e7zqUV9JLP3sXU+zagb66/BKXgCwUPibuuLVOxlOlZpmUCH2AbmEqnzZ8HcfhEQZd+78P3Ij", - "kqPSzt7NQf9kbfEydJqlO4huGE1L7mPzvphqnoKpRnmh+obfHVeWBHMG+szhSe/5d9/2e2eqKAtzlGXq", - "FtJXSr/XmUHn52pR895vHx6LrwVc+WJZ2zLaubN0c7iyyBRPB3WvxAGX6SB869ieMhFF5z0Oo3KymuWO", - "g1RTsN9FwbhOZmLuKBzuLDYqtDPIWSlT0OxgpnI4IBZS96o0B1fl4eF3iSMF/Bf0r6R7D2rH4/LmCsS3", - "hbyHolFxziv5ERUNuq+KMZojmZ77O17Hk/Iys6LAHp9K54NgK+vSORodLzsTdutvnPJB4KcAycSKObet", - "6hvt6eNFpF+pzMEU3etWsSLjCfji7wFcu0F9yaVwNPgrH/x+OPjzcDT47Y9n/W9/+CEeBfC7KEbYyHNl", - "i3+tETK0U/HRp6UsKJepJp9q13vYaS8kG+dcigkYiyJ6v2mFuBbSUeImrb7anq/GHXuZrFXgGtC9nxb3", - "LBaRXGEDoQKk/Qi3I6qpiAOjVXn6qfneCguqoNlA8j1uHEMy+00mWB3Rc0P/lj64DjpenOu9DHnUkqml", - "Fj9L/SUNueV888mjs1MsPT1kR/5XlPwUruTUGbKWWeF7KosMMBwrhEjfJVlpHPI69Qfbp0vFFAYWYPJD", - "3UXbsIRLslFg93XsDxKiP4xVhQlGhInQxvruD6F1Zbh4Jqq6I2StDC0pqS3vlQwFykuDzknsGTzzVJUC", - "ZXC5d2FtB8TkHCqo41a7gQX1CPXXdSWDx7PgCzeLd0QwrUqZDqwWBXOqo0wohhywwIBMxVykJc/8NDHO", - "+yMqgu0eovdXA9faTFdXqtsg3k8ZwSk72l98StqrCIH6pUYJoInTS2S21J40EFsbcHVj0ieCV6Tz6T3B", - "RL3iQl/XQNafFEIXIi8zShglqmt2bo4bEldgROaqA8fqu8F0Djw9bpi2Yrf1WOBqNy1GaC29varew35J", - "lFMrdPPg23WHJstylWm0YuXruk60DXbfZ9s4+USoH7eA3hf90erps8uwoWkFhc+GYf1KBtlgTN8CXlU7", - "4DiYqujgJ4LQaqPhrYHzKOs3Sp/F6IwCl+citMSoXsufDcR/EqkvwqJum/Ud22BuN7qOa31YWwq1FgyR", - "DwyVOnL2KyeV09x4qKroltWWvEIYeiCXu3ROxTw0QiTFNANuAHWrZn+pDS0kYxpP1RD1iVBzteX3PfmG", - "m+gzEZe4lbpyJoGJIxyWMGYKlhBmVHXi72QSfwHbqnL6lOIxXk41TrsYdUAnrQ7xGLf4F7CtwAaveRCz", - "CCtto3y0O8jHL7eqtvpEaL7am/5B2qG/BXeyT4vqb0IR0RZ0glSsUgNqTmO2gVira/8aPhrCgat10I2P", - "PLPh76/yEshOXifINMrNXclYETkKEcNCZ4WGGUh6N69Wq+szA3Al3WbiFecYt7UZfSrscKIBUjA3VhVD", - "pacHd+7/FVpZdXD37Bn9o8i4kAc0WQqT4Yz4uQ/nmimptGkGfvjox3Be96L2UfeJvwrMrzDehEZQUGnU", - "4+FLID4ROSxXWLwvNSBAEVs+J22BZHzTloR4uQXiN1v2dLGqS34DF83QyCfRGFdSNj94GK2VOBjIelBQ", - "inG90mbr5opgqTdA0bGfFKBVbgKrARSC0DaAU2VZNxOjZFQ29wmbVAXgQDnaDkmk7m+2oeM1OGlbW2zZ", - "+Vp1PL0a2MoG9W2tJcvUFHNFrUhuDNuTyvpMZTJxNjCIXcOMz4VDab5gc64XL5gt0Urnu/g3ig5gzBSm", - "VdRHIXdjSE7FVFZvu/Su7n6raIIP+UFPT8ukuVfNgapwvcA+xX2gFYmChUIseGCF4xAbRgaMwUBDAdyy", - "t2wwoKCrQ0YeBFLIyYcwjnHIi5AT+kTk18hSvi939Oj1mdiQaDO1rkDg4dZpxjtocyHot4M5+oDLJ4LL", - "cjzng4wcFET42UgtdzYyaqyDgo8R7uZpdS3h4G5k7v9RGPJiOTwZuVblIjKWOwXNqqLAZIwE2B4FJPSv", - "pPfJ1t6YvmMcmL/m3XH9hs7ny0Eb8buQ033/aq4WElWxMQZ3PLHZ4krici3PlAaeCulkuXs9u/c4RlGH", - "NcZUQrvU2RjX82yHs2swdgCTidL2Stb9yKrC2WHW4KVwM6Oi5h42fAqM0hN+dLzRASE0MdU5zzDU1Kor", - "OQ7q5Ng3YOBygTfNFqpkqcIQaAlux0eWZcCd0iqDYZniM9zX6Je8BuZLKg2v5HkInGnDinr/61JWFY/R", - "bfW8EX/ThI2HQJ/c631UjuUyxIZRkGCxGwIHiT6QKQXGVkk7FLN+Ja3m0gT19jkTE8bRtaPr8B+3b3Q2", - "uQ1ynTmxWBMdwyRDwMbEIRMu50I6fMC1KRA4AY+r7k9SycG3d3fe31VoVfCpE8jDK3mmYYKqtbseJ8YM", - "FBwzXsd1dME/jyl56MDf0Rj9eT66lcgmg+BdHFgtplNwetKVJBgQJQmJ8PQJrHX4fkxYhVs+ruj3EQMF", - "KCxo1AxvW4rvuHw1+Defe9OOXWI5L9j/+V//m2GMt4GcSysSLKJ8dnR5/BNbjZ6L1zz2X406AiUbOyAf", - "Nxv/cUVBjFe95804yd8+jLfcEI6O7saDdZtt5I5poGYSfyet9lkYsz0suXJABVcOwCbDkLBK9cZDQPUq", - "AlFIuekH/yym/VYJIsvcWNSsuBW21KLUNpFGS6KtiSN52QzzMWiFDLtPnMRKSqxMUk8xxMgQOkadGbA2", - "7mh/uDkI5cEhIk8fv4Ex427IyPPO1du0XA9/NzYWnYJpX2Dweset2BkMNvVpjJ45e1ZghsyzsxB/5UtW", - "YMF03+CqDhz0g93/MweNxvmowRvI3Pg9dLdTqB0b+zC/A1oFHfvjfUpQHbt7K0Y1SYxJKiCLJHD7eIZw", - "WDvjVXyNcfIOP7jVvChgpZH/RnD5clhOuEfI+Px15f3x4h28cK+58FrxXdmC+iwDOSX7fMKJ1iz79vD7", - "f6M6i/2a9BwAEwz2pTAK5BEeALSL6ww66mK373KN0lYnWIUbRO9BPZZyuLUoyFm5hJMVVuw5GVlVFvKZ", - "RFgbH+6IIjcmc39WLqqWJuT55Yta3aywwM2cwbLvavgQxf77wz9vHuc2mIlk5TnwOM7yZe0hPB867wlQ", - "4XL/i7y8iulOWTHjeMXNl8cR6jP0bE8rhQaf8j47t62JFllpVu4+VPw6aEjfKso+Es7tpepTGTgjDZI+", - "Mkb71UOy5Sqw3nsva3grtS75k2Hsg2OXO47jUGNiDhIN3MKo6oOBaFLGIobww6qIz1OFDbVX2QlVnq2r", - "OUTn/IzMC3RSxjHnq77+AJcUHNvcAi4n+OFTw4VWaTa0u7dfugIJHTF9GGV9v3ncW2VfqVKmj+jQxp0z", - "3g23oAevAdkrUnc/b2hhRbn/AYBCeFQwUrfSacyOuka/CywhNAUbq9RlSy0N4+yvp2esegs03hDhaVAV", - "lamrvwXUGK7GkPj1T4T+qygwIl/zHCxog+0vuho+VpSDOqhVla7vVINwKHzduXH/KAHZAb3pQh28Ng70", - "m0aMTXX1fttJOPt7fZDTy916OGNVOwkRq3nBXyJeemA1WYh7DRCihQdtHF+NTbdA2PD23bNcNx7AeXAO", - "ox7q5tpfi9dXcg1is78amzI1mYA2zIipFBORcEw9n3BDzz9a0OuvVzKF5p/cv7mmF+DvovAGF57MBMyx", - "XS7Y5VmQjOKRWQ2qcnf0pZBV/4/V5m/VcTGCYch+EtMZaPqvqoc0MznPsqY54rq0zPIbYJmSU9DDKzkg", - "SBj7nP23gzZNwZ71mU/8d4CFlO3993eHh4MfDg/Zmx8PzL4b6AsbtAd+12fXPOMycaqUG3mAEGB7//3s", - "h8ZYAlx76L/2AzzDkB8OB//WGrSyzWd9/Gs14tvDwffViA6INLBlhNP0muCoW0eFf9WVmvxV9fqN32jL", - "+A8Ta0mwK1f01Psgtni5ZNf6/wlrXDLnVewRDS6hdoNni23WUDWT35YnICfw17rS1/5zkbC76YR1Q/1V", - "hEItr9Gt/wtEm7+AbZ6gah+1Ar0KbTJhLOrpphNvXguD9Z7NPYXJl4kp9akjqFI/3zKqTfIF4gpm6yLk", - "KZFwFTewUX7X8y20dn/C0NjHeLphKGpt7vgC4YQnwGbe6OVaR8waeFo9uqO0fA489U/u7UgZFwsqoZv/", - "c6FmlViwg7pp0YN0CWT90TyuLwxZMGus5a6rkMMAMfpRo7Z8J3Wvlvh/uiSkjl4C966u0Sid71OGvkBA", - "XoBdJfRmW4ADbDtgZqKoIEwe0O4gLKxzYhqOUp87rnQdX0ICwYfqa8iV5wGUyzbsqDoR1INHix6pNJIO", - "F30Kxo42tFNw3/g+6xUH81XTvEK7TSOFfu++3nzvya+3unM5BrqFR6vEgFCqijB86awuUpxh4vW1JjkE", - "0+baIjMcDS8Ug4Yds6mejLCmtm2upK8s41cXcZB189FIY1fUT5sdJxqVcuoYCbUdHTxSZMs6ergnYv9V", - "FDVaNwD4PwbJebPg0RKKruC7N65sQPhdTaNddHElNxPGZhNpyyJ6JZdMot3ljryN89GIqzOK6nIGy6aX", - "SoRsETf0yYg2HuXTVaz17faBPr6Nl98bFjPC8r4OnQYD/GZQj9sf7lZDOcDhSdjFkb/D/+EsYxldO9jG", - "7XJBoqWXQKMR0lO9ASK9lraH7T2Lp+Kxo23P30vxjxJiDYJqqrz117FVvNpyvXabzNhj1/j7RMhGh2ka", - "qX2hJjltaGJ4Wwd/hCv/4MuYAxUpWcY3VdTotmSkQMODtzR4u0MFx3W2h82mhu9jhfUJUBTs/IUD6gJb", - "/oS48pi1bxlIB5Qj12lKoq7dr8xL+uwjwmrZLGThztJuo/agTf6AC3za+mY7kZzTuumNmjTewj6HEJuc", - "8hRP/UfvPwcXFy8HvnzQ4DLaiuINpIL7ausT7CqDrTd8SuLeMhPbb3nugpduhdVFnHIfvkQ0pe5Cy7fs", - "S54Q260w1j3m1wcZYVGebQyeJw3li68YPz+i3/td3ZAgtLHs7GDZ6p3yp++/79omtn3s2NbavpdEfNtI", - "/AeaY+9pzahKQn3pYhTNUk5yhnjIOlQrU1NzUF9s3EWnpoZIp4MPLyGE7y60DnMDo/EoXte3jXb1jy8z", - "UVmmbuORB63W340+ectgxgSPKm1PTEI7P2GY39oawuyWKrus0zh7fLX6g1FBbWp6n0yivVbTLUWZQ6zP", - "WnrFJIPbNOVQXly8JAIpMr641ZT2RkUjtyivWjX/OqtGs8QxW/SFTjSYWaOpLYLmzjI+5UIaeomHLARd", - "SizhLJVkmUp4NlPGPv/zt99+S9mpOOuMG+w5Z5BVf1PwKXzTZ9/4eb+hhJ5v/JTfVJ1iQpUG34fRx2Lg", - "jPXmsFSuLbWsW78F9IoZTvwV1Oc+JunwFC+7lbU+UdZDZB/uQuPJKtXlfo7lUOsjYNmBC9w5YUQEOT2B", - "EE9C6uh+6PsGW26hJ6vvU63wifCgtYMuDKirGWv/zWdRBjdRee64hFnIZKaVVKUJVW8DgE3Bb+VGCF/g", - "V08KYlzi08LYb6ELyPjzJy5+sgpbvga4f/h/4Nv8RrQrCEUB/bPAUjSb3+X1zGtVwkqTL0uRPuSxcC+A", - "utN8lpVK3/38RcYXOFYipu6laRULams3xlFhgI04d06f/Y/BOjrPV7x7vAAlrC/B2dnlfw2uqZXCZuQz", - "ltuy2xQZWD599bFx74nlGB0qJsL8L19klLIHADPheN2gT8UWOg1+9T+G6+BxPrH+RFvo0p9+XGDrDjK/", - "fbEWt1ryMcKztXioSrvJEFdfnirtWovcJ+JHD7AsVWdzw7a0MYXbVaUtSuqqn4kJJIskg68OlKdzoDSw", - "WpV2yWCmIcFyodOD2gkb566UOXwevn/SRO1qlc21ZZfTPf3AT5ei/YlqW1SJ3YWGucA3IyPgQsrmIgXV", - "8CM0oO6Tyzq5WMg+awJ+rfesclr51XWzyT5VIfNN/FvVXMtQq9t7BarhXY4sZHpxNxYf/H40+Ovh4M+D", - "3/7ln+7FGvHCDvLi+wenE9QY6WMeWwyu+nXwSkhsUj84ijV6FjkYy/PCMTlqzo+W3XpqGjxkfym55tIC", - "xctdAzt/dfzdd9/9ebjeA9LaygXFo9xrJz6W5b4bcVv59vDbdYSNxeVEljGBxSKnGozpswL7WTCrF2T7", - "pBqP7es+B6sXg6OJ+2G1FG45nVKuKLbVwA6QQrK6YX7ovqgXRAT1IapYtmeRWLYPX3DCKZXiNUiL1EB9", - "C46SCZIenfmD556wzUP7U1T5AOsESliNMj1XguxX6DU0rtTVLh8twY5nWXPa9rWtdECNhN49tfBtL7JW", - "9j5bR6KeCXyBFaLwBqoq7jVfG7J3VHK2yesK0Oz0BFsgYm3zqTAWuzRiyWrHQYarUFbFOiCr4ulh3Fjj", - "/uqVD4X7tAXDrSra4oeu2yQ8A6t+B60OfD/7tW1C6K3gJvrlDRUtdDNg4Q/F3Cx9B1yu0wyfLxP20+Xl", - "GbOaTyYiYUoyYYfsmGdZqBVydHZKJbKFcVPeOml1y2+ACcuuIeGlAfZeihvNJ5Z+DZ3HE9/Y6QZ8k5JF", - "KGIQck5+eRMt9UHHvHAnv1R/Ba1624Q14vcDqwbulMzfVfoowDlNIS+UJbHhZ8Z7hXCrjSsargIO5Hq4", - "nYOxSoPxZTNp6uooVSeCeo2+47/qFlUIvM32ZkhrQI1GpBkQQGlspeb88oZJ5UuJYOVs43WbGWQp4w5s", - "US+7fDhsQD4RaGjiTZCxkEHudJ+NhXaaDZmqUe1Se0MWPv7+8HsmJo3vqGp3XSQ12nrmL2Avq/08ofWr", - "WuTCchs1u1/GD3hf3W21u1X3/FXlyiV2xrVvgkH5rgSQTkCgVEu4hSlV4oU7d1nCIYbB+hHNOirsWqUL", - "rCZLQd3pi/CSa06hwXIaJ3SFCYY69JudQM98X38Gc2hu3eGrX5ByYog2njPs8s+SDLg2oWhT47SxLkbu", - "FtvI9ASdeikAo1qmWXDz49ly743NX3DmtC/4uY6MyljXHbAb6CZg8beHz9pYfMsJjRtWmBqjX/jgLDfu", - "0I0T1g1wiJ5BEgK4VGEHQj5nvBb9M249lrvZm9S2x5dK4FKynlQWg/sdETvFQZfQZ0oHSgrEEyT+fifR", - "vCBB4P6vkhpeIO7GtM9K++no7LOnq13zl55mQwY+bYDTxTqR11JDGgkpcfXwVP4d+3VwSb4AJkJuar0A", - "uSb6bMp9Uz9MqkywIPnyNpokf0g0hl8bI6YSUgZyDpkqoFYW/bKG8TSYO789/D7y+0Rk9GzckyosH0pc", - "+2Qz/PYbUxOuMDXtImF/f3jotLY5z0TKq+75Hc0+zsrrTJha7pED54m8mLQWLvGJvJj1OT2QopF+CI6C", - "dutYdQXRhOvQpaCGN/UbS2BI1BvR32lCniRQIHqVtob0elx7QRIkbOUBteHbTQdpwi1IYpnYVhydyxZ/", - "kNQ8eA6s7fOrZyaCHbKXPJmxieY5BUJj+Q2lczYW6XP2h4F/fLi6kim3/Dn7I4Bg4ODt/n51JcdOWtLd", - "+14EVYu4BIwZ5Eoqq6RI0MFYgDZoeku0MmaJ3fnUxBeMs9fc2AFCbHB6QjYA7JbkpbgbKGsJjVSGD3QN", - "pszDs5+OPWQnWhW0KQqqIoBPeWGCQj0W6Zh6lGBHIm/DADGHlH4ThqpY2BmX7BnjM+BpCPnO3F4NgMRP", - "+8HXeQvaMQqBectVn/jrcjIBPWTHmcCvfG9Tq3lyE5nNKQspWEgs7nfIXmH0e318E/SLpStDk1+9bK33", - "e1A5YGBahQHAAt+06xes4Maw8f+tocj44t95lo0pr7w1ncpSLHqJTwvHbT3+GgvcN366Fe6+Z7zANA1s", - "hwgStEjYuM3nxtSzNWhN/vbAP2Q8Zf6MrU+osSTbc58vsAWTwzZqFMhZqpIyB+lGje2igDE1EauY9Zh6", - "pjicUzqviobUDX28vvLPuK0T/JhYVp8ZVAhpPzR5tMMgIlz7eBsr8507lA3dSFC5M2168t3ClGYGZMoO", - "I/AI4A1t+balyT4zqk1Yc56VlNOQgyMzrSHBWhC0FHdrCGmH7JLfAHZyTSDFhdCPPSa8GZNYxXaStDC2", - "KsPlHEPipVUDDR6N6+Uy4BIbZSEikdl/QFM6CM2EwfKWdWVV8ibVTsgWEeyWZHSGiL8Lwg/ZOdYARpJm", - "ieMn3LJnh99+/wIHVMjMG5wAw8RLPeEJUNHQidDGErFPMcNMey4z7CwgSzcSD53IsvvVgH1A8MlW8vz1", - "FsLoi8t4Wj6Bg+gF+s8HF44ePQdwo/+/AAAA//+OVZodEMEBAA==", + "H4sIAAAAAAAC/+z9iXIjOXYwCr8Kfv6OaMkmKVV19dhTFY4bakk1LXctupKq255RXxLMPCQxSgI5AJIS", + "u6Mc30N8T/g9yQ2cA+RCIrloqcW3IhyeajGxnhVn/aOTqFmuJEhrOi//6GgwuZIG8D9+5OkF/KMAY0+1", + "Vtr9KVHSgrTunzzPM5FwK5Q8+LtR0v3NJFOYcfevf9Iw7rzs/P8PqvkP6FdzQLN9/Pix20nBJFrkbpLO", + "S7cg8yt2PnY7x0qOM5F8qtXDcm7pM2lBS559oqXDcuwS9Bw08x92O++Ufa0KmX6ifbxTluF6Hfeb/5xQ", + "wSbTYzXLCwv6KHGfB0C5naSpcH/i2blWOWgrHAKNeWZgeYUjNnJTMTVmiZ+OcZzPMKsY3EFSWGDGTS6t", + "4Fm26He6nbw27x8dP8D9szn7e52ChpRlwli3xOrMfXaK/xBKMmNVbpiSzE6BjYU2loG7GbegsDAzm+6x", + "eSEOXjMhz2jks27HLnLovOxwrfkCL1TDPwqhIe28/Ft5ht/K79To70DY96NWtwb0US6OeZadzj3Al25S", + "sp+urs5ZwrOMTblMM0jZaIGHuQEtIeuJGZ+A6fFcMIOItXqVCbcwUXrh/g2ymLmtORzTKqttzVgt5MRt", + "LeV2I3pFtn/ihjmUUoVOYMsJcOQljfjY7VhdSLfddPUurnQBTIzx7G6HbCwgS9ktN6wcxdICHCIY8Tuw", + "TMyENe46/AlHSmXAEYY2gli4FWbFDIzls5wJyT5IccdmItHKQKJkirONlZ5x23nZEdL+6UU1vZAWJoAk", + "TX+pbpvnYuBgGLnuJZSxJkzYreBW3umWiHTiAbgDzZ6D7iGW5XyRKZ6ysdJsGPY9ZODmNau4lRYaudNg", + "FrnRX3mW9ZJMJTcsfOco1kGQkFm7S56JLBO1+/UnlMVsRLfp1qNFRAQv3ucgj87PWPnVWRoWmTk2BCnT", + "yvGbPehP+myYa5WAMY5FDLtsaPkNXCYaQJqpssP92g4qitDEB6Pru5vzvzOROoY2FqDZWKtZC52Gr2ci", + "TTO45RqiixrLbRG5VeQIQYgz+oolKq3PUuLiEnrVDrJ0r+V63QZM12CcQ7dLy5Ob1S0en5yzi0I6Wurj", + "J1eaJ8A05BqMuyI5wbv5Dz7nlziOWJxx3zJu8Uc3Ghm8JOzrs9eO4g0rDDC3guQzN1GipPsZhYDmdgqa", + "2SmXzEh+A4OEG2QJiAs47/FUqxmwE5hfKZUZdq6VVYnK2K3QwIi6+9cywkaz7LXmM9hCKOFpxvhxlzns", + "0zNlLAmghuhZWkJlxUy+I8xfWeSvoFVvxA2kjD5kRCPsVtipIBGXCRnFg25nXEgUR+/4DFbnrkEifOju", + "F7pMaQaz3C4YYSYyBi6VXMxUYcqPTRSF3W62OI37LHIW+jp+GvrtLI3jHv13jRyjuyt0tjr8w8Ubd2R3", + "9sBG/GxjkcUIdYnCGtdc2yct17iSbhPeMVJrqhdLTHuVExKzZxkfQYaAwu0jUVmkQOKB3CxkwhJeGIjz", + "u5zroIBm2ftx5+XfthLmFUf4+NuKgMEpG5tBTMKt4F9Nf+UyayS3lhHlNpnyS5XN4QJMkdk2dYol9Ckz", + "7lvGrXWozTRwlBOcOUIV7gpVYRM1gy2VKZr1ocpUyzm+6VWtepW/+AGCc6Dxzp5Qx1oHoN3VrYB9DY0r", + "dqJ27St8He5liRN6ZJ+DTJVmYz4T2aLv5F1aJKANk+7GMwfTXKu5SEH3TA6JGIuEWW5ukAsaJqRVzE6F", + "YQbsSwbuIZtrYYDNuRZcWuM4pYZAXInKMp4bCANBaDYHbZxMGRXJDVi2N3/ODtj8+/0u4zJlXC4c158w", + "qSxL1BxlKfEqd7knygmit9YfqMvyjAvJ3h9f7DNhnFqhtMNSbthQOQVgSPI7oMk0EKjDg3Bn8+fN//ze", + "IUWhpbEic5gxAbDu7dvt4JRx4t5V+0WtkJiPsVxbR1QxnrOiA+OjdeC0vNWFEB9roMNvUSN0D98xF1mh", + "S/X39OLi/cXg+Oj86vino8GHd5fv3/xy9OOb0+F+nx2NnHLmBpkicUryTnrp1fI52NBPM3xJZ9ZMg7ti", + "ZLWF4aMM3A/4Uu+zod9p7GvpD7VnANiwugy366FjLaqw1bhUpIhJNL6uUjiBAvo7w265sGxUpBOwfTbk", + "Iy5TJSEdvvSfsITLBDL33vZiNOcTYJLPxQQ5Ir/lC6fB93DNJr75YzueRkdy10ib7HQ75WJRlHJ0F31n", + "eChzY8TE3UlNuWHvc/6PArpOMx4XJPlNkTuqYI7Hmp6GMWiQCcRBegsjIywMpspExOZPipTa8hZup6DB", + "3yeRvJMWeBHp2vlzbqeRFxS30+3nZ/93AbrURuEuyYo0uuyKLlHjlfd47aT5sZISEttuq4E7b+JLMuEI", + "iUguKYxVM9Ds8uTnLjvP+OJWi8nUdtl5kedgAfS+e8S4uSFlxDLxgfMrjC4V8stcq7sFmbGEYb+83drI", + "4yZ1+4uh2jeFYlWhSPOBv7Wn1CPS/ESYZFd0SssxkFb2hQ2Iws65oFcVfi1mM0gFt5AtWK4hgdRR0bB2", + "7mGwlhr3BDJWA589CrrtogmvXNA3JXgtzlao8UnR9p6ab7XbJeW3cZLHNzpWCLqV3XEGxvAJDBJVxCiU", + "nu1ubkeC/mOnjWZ84RQElLyRdUGgjSoVmv4WN3Bo4Cb2yP91ulieE6QTgGxIbGKQZMo4JQq/Is4hpLAC", + "cZj+qIzTzoqcqHuQTLmcoPKDtjFRzJgG1E8hJR0HDGrvTldHKY1cxioNLFW3khlVXy1RRZa694CHMZ9w", + "IQ0Z9STcsrBufQuo0g1flr+xVDhNUod7ZXkxy0kJpLMqaeHODko1zR842Fb970jBlSq3Zxe5cArewjtL", + "mJkW1h1hv6nB1a+y0+0s31T9T7gntOUs7WgzJdbxeBndSgxYR5BKGpUBuvpaTR4j+tbdiPvYK9JKM8fW", + "isnU1q2wcJdATkhFJtfTmbCVuLlVTghZIROLSE88w5B4ScUYlUxLHNRMeQ6mX9qB/fpH52fHnIDh/9L3", + "7xWeZWbfoZZ7nRqWwRyyLnN32mVcTww9FdFUNEADUjV3ue2rqXb4uFeerfylPjXNmQkJXW9J7fqjDAqd", + "Rdbxhmf3pvAeWfd08ZoajWRcA+P4gIobj6Py0p3/wcJyGQu+ycp2WUl35Yn2CUVlFCa72lNx5DHxlc7H", + "7rK3wBFFhOKzrKR1rifFzM3MEgU6odcFndX02Tk5Y5iS2cK9uaRHZU/tbYTb8F+svl+XLNZEXxHjVMOD", + "0bD4195/FT9C9ELq3nrjS1whLmeRzcS9COEW3SA255l7YfPsli8MuyaDzHXnQbcY9Zes7uVNzT3y+S6q", + "YpAtTpMVZwmzU3Tlabht7vERNtYwRwVGvbWdvXRTdDtIW6smj2LGZU8DT5HTk4Tyooh0umWxeotKTypM", + "nvEFE7bPXqvyVxJxe/sk5Lq1YJRAoXUC5Z5EaYqIKOt6JQzG4g6FFe3PKxBdhmaH686HMBLZ0Es2Ump2", + "3XGiv/bbnpBOMM6EgX12tcjBf3zHhBd4pY/vukOSbQPPdBe6yhp/W2GOb9Rka6UlUxPSSCqtIVOTbnm/", + "Qo5V9V+3XMsuA5v09/ufQRKHg32TwxvlcKYmTy+FG/D4smTwTqJ0jahqVbLdHF2Wc2Pw8adVMZmyQo5F", + "ZtHJguyWIiL63rA+RJ+KKrwxsqEy+Sc5c+854OkrxrOMoX+ELUtM41Rl4Jo5GdVnl0CmKpNDUrqmx0WW", + "MYcTUcbyRLz9NTLeZfCsQmezSZkA0t2C5TWwaGVH/iPP4cLTFYmuivcLLHGmpLDuBSedqMgyd6u9ID29", + "xYSdBecACSvL9QRslyJS6H3jPRn41MtVMnXUfTsVPkaGdqKSpNDuvR150OBUUUeFgzL+Wg+HqvlgaDNx", + "/UfxFHTrrKlKCFb0XW3+LnMKBbqugCfT2umi60g+Hxj4x+oqb5VUVklvIxAycY9wdExW10Vxq0lQybr0", + "mdsXpOUGrMp7iB71kdFL2IJ7evNL670E80w9/MxTGK1TsxZF74O+is4fcNNPVFtiz1hUjryhqzqnCQfl", + "zPLR/roVg1zYgrKvcITTUNbG7mjIYM4leVanwhAqvyLHkvtgjNE9JUwcLeBvRDrd0oJUfgv2VumbmjFy", + "PVOoAat+sc0jVyi4RnzVVYEdjaxazUFyh6QzsBy1Aw+5hcNmInRvD9EMvJGnpPxVrQnimlqIJag5n5Fz", + "YPiU9zi3yaYhXm+de5UmKrzqOOLcCJm2qSrhQH00JQdzZizUz4ux0onimWufDSlcc8BzMXzJfsb/YEfn", + "Z8FeuOf4jJ4DWazpj70JSNCoboWdsyHcWZAOEYYvmZB/J6eN30/5W58NM5XwbOCDUocvmVkYCzPm/8B0", + "IaWDGM+UnBiRQmO7TZtlmne6nWr/7qewUMfx1tpCUU03oEo7skWUlE34EKQZIYPjVkQHB55ODkhUnJ00", + "4B1oYYm2EPhrKOYna/OfwMkG034Iq4sVgsGY2imNZDOeO+jecp1iUElPeExxu3esTRW2jJ0hIcN+4Vnh", + "VB6Nyk+wMZOWx0aFZTO+YCNgXC7Yf1y+f4cqUkPrWTkMJoxQCsFxJpKbjY+lAl9M7tOgSfDcFk7Lmwte", + "ISFyuyq2cvPrSFQbeegLKXqmb++k1ndS7eoHCNknfC21w+aR30wGMkisisQEH19esvAry7mdBhs7nt3x", + "1wwVrRaVYhILln/7hlk+aQT0Ls3mAFbkOWiMFSdG9eOHq6v377rsqMtOzn5p0WGiyvwvwgj0Djiu51O5", + "WhbuMqvRIR+d/i42N9xiVM9dL1FKp0Jy2zyVO4u7xVzcQWbilrzFmokX9594CQ/vOm6lbgVtgtDaZ1IN", + "BX+GxUaGdwOLkeI6/RrYXTjPN2a3FbO7gcWnYXUNuDwyo3OHWLnAn2FBzoRK+/zZ4zHdLTGgU7fFLvuR", + "Jzcm54l7tce50D24aeB7aJ+fYvRFUhiyw1PK0gIxJtdgTAt32p7b4uTrue3Zu/MPV112dfqfV0cXp+08", + "d1kdhAcwmMtEqyy7BGszSDeyGoNfM0Ofe4YT3k18bKtPcmVELfUUIwaEnHS/bPa0ehvfGNVWjIqgPvCI", + "8Wl4VguwHpl7OfY0iCghtDq765WY7hP2KKK98gO6ryZgHNJvo5bgeovW9RaPvZ63x9yDf9Jam9RRFbu8", + "1xghb1avEFmImzycILCabU6iYvfWWGrxKEst57oRhpSg84f2G1q94bWs+Y2Yg1NDN0RZs0zMgc0F3Fbh", + "Zkuh0+4dPy6ywLu/M+xXGF1cHZc2nHdwo/b77Cf/nZLZ4hX6OgNDHyuNs2RgDKPM3U8dAhu7jm8suZUl", + "O6wYOKz4BOHbraDZPRI2WO4bYbArZ2mPhF3nGXhTEsqqf6DPLhvG+zJY03SZUYwzq7k0SF7B/j3KRM4S", + "LpFIMBTQG1HL2HIMGB9WWxruZCzf4sI3B82vcod40Py2LKIKno9BZbRYOe7nYBHfQuV35xKfJGB+HYAe", + "nVd8QYHz9+VKr3zFjhA1r6nKBaWotHHFHT1yW6Z7vSUv+0mNe7TwnCufg1O7I6uCp8dRRaaM7bMr1BWt", + "XgS26R0CqVZ5DikrpBVZcO4PSn7sXpdaizmYPrvSwC16EITs5VpN3PM8lFjCiGULbM/z64FIM4z8mMAg", + "4wtV2PBG2WfcsEJqyASKAFrZTkFux8D8Hh/Kvdpu+Bv7amVfATvqMu0J2ddaCG3iX008astmucC/l9EK", + "1cHQqZYgEQ3KXJTSoVt6R8Mv/bofdGnU5hvanGnhr+JMCvuai2wjMwi8jVJh3NNiBD4LJxO/034/NaUt", + "bf4bnW2kMwewwRiv7OnJLAae3YjMWMjbUXIGdqowm73EQx/PZCEnUzAd1dtkKd6mb8AeFVYdWcuT6RY2", + "WdzE5tNeBAG3FTlFZWuDtjT0AOORhJmWFlm4m/LCWIqfyKpHDtmQsPqG6bN3io0LTXWjloX0rcgyL4DL", + "pFpP25+DhGO39o2ON9JxCfhPRsytgHoSsdlAbF9yol/9deDpwAlQogOH4YEA2C1oYOihKfIyvMWXsBgX", + "WbZAMat0KNrWJMi65I2s+IjC9wIerIovnSrCMviyDnJKjCBYBtOivIcJzzHeh/T746YajmVpDFg0pyyF", + "GwaLitU8uXGzeVWFjTWYaTBSCMNyJaT9rHzmG4/Zmcd8UvbyENYSaHVbowDWY1x6/jPLbwCprJbuXfoX", + "mqS0zf2u8IbYJjffT1XQs9VQmIMWKhUJM+W3wdoRfL5zHxSzHQVW8zwSES4d4hsNbqTBtSB4ZBKMQWc3", + "CsxlJILiR27gTy96IBOVQsrO3/1lSwQtr220sLBRS3drrznjO5JQZ2kGGyMjgjQTaYjcXoqL4OyHw8OZ", + "Yf8oBFhPd2RTl4oJ2RtnYjK1zJe1xeD7Lb1tfumH0tuSH/wbha1SWN2o+IS05fHujeKpkJO1T8NVBMxo", + "VHjF+gIWZ+NGXRB32zzTwNOFux+Pexj55DRHjs9c9waWiuVaKM2G4ex+iiHOUfcUC7vfZcNCZ8MuG4a8", + "KPfvMp1pSDlXQw0+i9pdwLBWMuIVG0aQETPxcq6pRj7LVV5kiCWYRMQtS7iBbatNPBKxtILom3zaSD0e", + "Q5/+FboeSI8cJ0QFbzbBrE6AYcRyaiOG2UwihZ9roKPaj/HQ63chVQtTVWu/eZOWBPvy5enFxeD4/bt3", + "p8dXZ+/fDS5OX3+4PD3Zve67YxeRuu/owQpPRKXFREiOFqglNtLqvHKr1rhEfGF/0v6F//RqkUPNHIAr", + "rKT91jNZfMbvz1LdSgpHNUxIrKXITnyaZZe9BptMu+w/f7roMqoQ1GWXdpGBmYJ7257NsN7AW0gF77LX", + "yo25gjt75V62XVaj7m5Vo67L3nIpxrjDcw1jWuO9nYImNjlTeotC241S9jWs6FYIuTbeyF9h6H6zrZQJ", + "4MPyFS3Jck/Pfuu7/sZ4NzJeD7Sn57grcHlkXhsyoDeWYSlTpVFPaNZ/87cR5T3TWvbcLvuuZ96tFn/3", + "1xIy7PpuJb8nR7atbO4sfNPHGjxCptgMCTNYUf0pTPNM9+Z5xnO3nGvj+FCuwUlrYkhY4CB6XcIMNFAl", + "v3WUg9ZALyqM368pMupfxMIMcZIhv01LGxDv1OGGhcrNbnJsZEEi7y+nV112/v7yqqXQvzJ2ENhPHGYj", + "lS5QtLhZDs4/XJWPtK47HJ9zkfFRBi2ijI4Wx9f3JB4zzLUewVj5YkZhFIIBD4YKeu2y8Rp1AY8ktbus", + "kOIfBTS6T1Runm8S+uES2qNxt8nCKoazwhC2E97UBWcH6e3b5mhIQMyrZ+Jrt+ma6bL8ENHfAcX7DGhY", + "F/2OiJUha5i8hJ9HGajdwjdtYAttgO7rU6gDy5B5ZH3AYWcUSB4SDTSu2CmWXRv7kmbs7dnbUyrZ80lV", + "Ar+zuk6wjazzCo4KsmOdNjMTszYeXR46TFheFQlOdzMHUzvLumy5CeO3t+IXL4keqXtamKbF3hCdq1bt", + "4v3PXVa229y/r8AsOxUEQlwrGc/5BI41N9M1ltOcT+A7p5LKFDToMpwuoXFsj0t23Tm67bJLyfP/33Un", + "BBXss9spFXasjDZhsLAGsrG7Bax+nTlhyC5CaxavmYYV/A68jtWt5RDUK02HkkPYwHNI2NsPVUqGfXYc", + "Mip9GcmwtaGbfsgCx3biG6TTUdNtjaVugodK52VIfJPMrZIZo5Q9bjyhVI5CZDen3ZpKWVVtmzqPDxH0", + "NcT/tAWxqq4qHOmInlK2nfw386n2qlZuFxsAcKJmx1QV443i6Rb+nZP3bxsDQiFQd99uwn5azohzoSq/", + "ZeHPx6Lz6KG+Efx6gk/VbOALpKBr5Mlpvx1Kj+0SSfNBeW8RTkERabNQbJBRgA1VhRCSheAabn2lthUS", + "GLv76GKvCSvmCOJleUwhZXvunYpQwyqP+332wQAbWkPV126b4T2RbJ7lLkqNk23URN5g5sm2RRYoT6Wl", + "yMIzfy3+kY4sDfOgqlACC3oOWC4tzDQVY7RTVYbDuTAFx06zI5EJu+izU55MGwMoco/sdM96flV3aP3p", + "mMq3mITteEgztemJ+YfHZocjmytXF7PCE2cDt/aO31zue9Qu01HPQeMFyATYlZgBNsQ9Oj/7tEJs+Xjf", + "5Nd2uOcu7BNj3pP4lnyI5epFniylgzYQGqTVi5W40D3fKOEQxUyDHbMcNJaB3o8mj9ZvdZCC5SIzu2fL", + "BnKqXRzj1moxKiyYDZSHR1qlvSlPBxoSp64ImRd2PUo3LslXU0ogpagHLNWIkwSXA8bIdX0/QyeohOcP", + "x28u4yiP6kIkwba+rkmUDsYefAU7WO05pQtvIkTIv7ncj4v+FZz01qYdqz+HSlD496ppReOKymLT0deR", + "iDUtjwKvovcYtm5OX17OZ1o6sN9LlUi8hRKU5BvFxRv3jDKWeTVvXGTsnAv3zHlzfP6lygt/rm9yYoOc", + "SPKnFg91SDyyWMiS/J5s2ON0hdKE0Q9lw77oUpT7iLSaPtD/m+PzquCmGAcnSGsB+kGc2biXF+VArM67", + "VVUEqdJ2lnny/i1zH0S4Zm2dtlaBMgXdsu0L/HHbjb/yApu6BpNLwhdAKlPDrsRMyEnvKMvUbY9c+PEq", + "EOJ3aC+PyjXwlg1R/Slm/lHwpjyo5t4U/lKfEUN03RGY0mwuUlDhp5Zq7k8r9OpbczzMm+EeX+7hQjHl", + "7N5Cb7OkU3zzK796uS8b8rIw/HOY8Mq9fxNnG8SZ4k/+0G7A4gs3zqGOWaHz12Kae1cmpW5HsfUOKL41", + "7DL9Ir94F1rk7/fZMddaAPYGKRsBjKmXppDItUZYSt8y3w7Dt1cLbTvqlrjlhjWfljss3dY3HrGeR1TA", + "emJOEYPLbh69+0l1GbCcvti1m9E7uGXrOxoxboyYSJ9ihCSxoalRzrVTi9vPc44frB4JO5kUI/p7rY3P", + "K5+cRDuINDQyLQWpd+1W9Gg9iT6tZ7XCAaserS8QRUXWNK8Ki7YmhfX+ltDSGR3BLY64si3SkoGdTfkc", + "2EjZKcm5Mo7INHGn4XIpPdDCsNr05InBNikYP8zOZAq504apYUI95/AV48wIOcmAuS+oaALFRqUKqFHl", + "CGWlsJ8yxuObm2ZXefCJXDVXfPQ+B7nG6SjhtlRwLB+5x6HnJxgogYNJt/GVkEJu6JWiPyDuI17TOLNP", + "YcQmhLLzRikwYarsUl+o2G0htOYzqlFLdFMmqdeXmjmkNcWppApEP6dXxvJL++xYSVPMQLt3KKXPLulp", + "2Nsq9DOaYskli3UIhXW6GkdLvuDZTrmoj6WVNaH8TSlbT4SWjwaE15+U+O6hk+Eu45rTVVuElaNhTHby", + "pIvEoCRQlopc7KpkxMO5gryTcJstyqX46Ek0DytsFjH/UFZU5nmP+6bUSpGhxDcTVWPCVDXTWfscjxUF", + "dgl6LpKNUaozLvkEUizJLBJgcCcsFkqFuxxL32QLpzM4rQWxyjfcoNBbh03cKt2rUt/MtLCpupX7LFXI", + "Gn27wDrb/D//639TMGm1Cq5rKCAV9MzHnOMTuDcRc+gVua+WTb0vU7UtF6RWfw/lg5Hb/MYIWxmhR6ZP", + "EGzaBpfdCz/jXptln5eOUVZ8Zqd3wmI4OyKse0HyDHUEbG9w51SBMu2+kCnoDBuCBssU0Zwuax0mUy4l", + "ZKieIF1QjDYRJLFFu6B29JkYQ7JInIY+5QaqqNfSs8uEJO1lD58eZebgPrmAzk5wo9qHjMeoCGeO1ZTe", + "Yuk+GyLRFvmQzYBL45vo08FT4e6F9DaBCRfaaUSorXE2BZ7Z6aLsxok13vps6P87TMhZrmEuVGGyRTmm", + "sUKTeQ0nfA6D+IYCJMpKej5ml2yHZfE+hLKlEtJWO1i+YrIqaNmGKFTY0j3hKvNCACvVgzZqBnZaq05n", + "yqdVSUt0nZ1ux99Dp9vxJ4oytTxqlDg7WYmRpivos6NRlfwZuxu3GCvy1Wqf0WsSTiFmmZJuaFl7j1PL", + "gPOzk5YEEH+Bkkc9MVpNNJ81uwv6Y4T79G1wsSyxKGZOnZ8V1oJ2/6J2rT1ysfV4Lobb1Fit76nrqWId", + "K0JB817NfhZZtqZ24hshiztGW2Lv37/t3Ygsw7KoKPewpFOV7yHLdrS/vO2zS9/DF126w4MU5gc3MzMZ", + "hjeRQzMuK3LAqUtW5Nf0QmMGM6UXJUDJnBDiYrx9voxeN8XIz4l147kt2Z0pcndRZvu0j0eSyCvX/U0g", + "twtkvKyBUrOBQ4mnFMhxsOwuj90+l8Rx8xDtDRgSJY3VXMQo8NdpkxYgESnZCgIp9tlQKglBXEwyNeLZ", + "KrW8YsMZzJKaWEomWhV5+BKhj9gxFfYVGyZ5YcAO2QGOU3oxyFUmkgUZF959eHt0QH/opVrMQSLtVuxZ", + "Sb9lw1SGsQZTLtkP/UPvH0tFWjZX8n27dJFQ4tZQqRke7eWQZUJCU8C4w2Im3CxxsoX2SX+odtnSyno2", + "GGuAwc0o0hhLA4Q+2/5KhGQ/ix9DY7F6sITbXJeloDFbvAxYGbrZX74belITsga67wx7C7PemRwrlhaz", + "vM+OjClm4CDxAtehKkfid+izk2CoCRmVGpKMixm2ZkicAhJa8pgZzzLvh8SYd84y9+xCqA2ssjwb3IyG", + "2FjCWIejDvx043RYB3K3FCp+bMp1Si0esVywh6ZnIwEJ67DjVB0Dd1Ye0PjqnvVW1jVyr28twrjcLw8G", + "xTu8T8Mujt4SFj0AHE9zC5s0Hy8Mg+ITn4N+bFFEjtVsFp+NYYyJT8Nsitu9Gb9jz35wWr423ZqsaHzW", + "ku5sTBSkF2DwXcAMWBI28V15MO+ZAvfNpZI9bUyXjUUG9C/UbaczmLn/3O+zK6el+vop+XRhRFJxv7p6", + "6NC8MKjcxZGorYtePrDc3JgYnuasUjJGWBEbT9kzYHt4Sr/UTM1q/Z4JYw3dvZuSfBdL2lIDVYdXbgv0", + "whgy7xk5neV2sQ4pvQHMfXvM8THALfsBw3+EU4sUG6kCXToktRDZEVmFBar7u6ti4/aJFM7vzmiOH8pb", + "5VrzBSktYjIBPdhEAP672lN0G1L0TSBl6jjZ8Pj8w0v2zmny7n8cQbwc+uoCNdkSgXvY49YEViLaVBlg", + "PMsUVQcofVK1wkR+31YxIefqhhTmSrfus/dj65836EPjhg3rOxmyvdo0nohqmfug9zGIIuGSpWI8Bl3v", + "44uDEtqm/9nd6VwkVsz67O029N+4t7Z6svW7I35XsohtVTJEqN20saPSKeghQvFum6gKpcCKbrY93B/C", + "NzdRwloZsD3TbUrRVa60RXdPAqKH6GZY1kzX62zp9ZIbZMB2TzZviiXMLqtdNNxF3c6IJzdOkZXpwP8l", + "PIRvlb4B7f4w5RrS6r+xcldUQwy7Dh0/jukpIcAcKzkWk/vY6UK9gaqNiO/5jCmNQk7oGRy8jq2PBJ7b", + "ZLp73NvyWRb+JKvFV45pBWZUNodgJWGqsImaAZViqbUpfMJ9UI9G8osepGAxFbO05gVni0OfXKu7BaoE", + "ZYvHsE+jyEvyVJukFdz15IVle5madNkt17JLhUb3cVeOBRSTqWVwl0Duo2Nof1ar7An3dzRxioh/mlGD", + "aMP4hAtpbL1SR5eZIpk6ASMkaQMJzzITnNErlijfC9MX66uajj/dST5QVlS5VDBW7iWZSG5Ml93AIlW3", + "0r2IsK3xPm4uVNN6uo3Va5MflAVfQiWSPkVfTZ4SCc8xaLqkjHAx9Ygbqle4lPn85vicLqlWG/8Jdxma", + "KoyaZexDofpaLXvTZz+JyZTNVVbM4BVT47ETnymMeZFZKruY256QtHsyzD3dzkNI2S9vveG+ohZV2J4a", + "9/xDA20fVHqI/JO9YOclu6+j+o9rpGTLjnaTNjQoZGWiaTVoQnXZQytEMmioGE7MBAXUBwt9P7XyOV6O", + "pX12Jlm9BqRvkO4DXAhiL5maCetDv4TxJpI9z8xvpwotGzT5PsuAz0PHz7CiGo+90cOt5Rc3DO54Yr0T", + "KinlNSo9jonpAmh/R1fHP9WqVLbtxvhIMuxp7B5YBC02/OPjcB/jdZhUPZW/am5Og3W8Ff0x6F1yehc5", + "hK6ULzTElGapMNRiuRo6F5x212ULVbBZQYWEU9zCXZ6JRFg2dAcZuhmGCPxhQwEvLbVbIdl9kKvqVJpE", + "0MwLkz67XAV8n70Pz7Kg49zAorzr5Yved1ALGhLarj3xG7AvGVbmvwWsz0/efO/lR5+m8VmsKuvW2j11", + "vS2py7wOtd9nv1J+7dDvaNitHJk1HHLgcHjkSeMlYhPaPwPqv2JcLsgjpnwEpTv4eNxnZd+par49r5d0", + "QxgjtWLt1sUbKQ7DiieSwTXCCRFRRuCuGX2ZVvXZUXUkD6hQCYM26U/Ckgy4JvqyccjSAYa+i0qtbtYe", + "NdPKqH2/Z4z75Eiz1RwOyaeg4RXmLGfq1jBeWDXj1seNuQcpelR5/ZqajCXiofHH2zZ0plWT/9jtwJ0T", + "P7vOdIqjwizbEFxjxG5k91rpW66RIalxeS81mFlFbMKCdnSQgrFYL9ABcCl0Fqsj1273JaML8BjLCpnh", + "a9mznGxRYouDYxfVCzIjEGNyQ2vr4dQe+kEmNHiuGxaIhPhtncsHnklvIqc5T7mcxHFA2SzfFWzvr96c", + "7w66lVG7gc8NP0A90d9XePLdQ/xGgC/w6egQpAofrUDA3OpNjHjf4CJ99hPa/hmMx07E7YVNWr5w7wHH", + "kOZY3g8kfrYJ5luLpGPv9g4qC5y6N9OutEHlB8mwM8Ie4SkLDvVqWxi+5f0oxnufKCKEOm6sgmIGxnjF", + "fdVSEw82KVcb0NQznhtqgIYxBwdjkQFJIe81O3AkK41Q8iDXyv18kAqTZ3zBnGB4VeYR+AmxWLhTMXx4", + "vcN4boUvV1RvdNzcCRpr6jNF7R7xJrHvcy9dq7tcbgvbb0R+qHwQ7p9KVGpb/4PvwYwX4K7acWB/Ce4/", + "/PnDh6KYDcYZnxiCj7uizX7gcOYAwphp6ti9G9+qwoCvZr5jHOqosDZWEAWnZPQrmXZJqKMErt1TBmPb", + "6Xa0U2LcVkWaZsGSRe5yR9FROOGTt6Wq65W3oeE3/slfW9U9kTvdDkbm4CfRBaYqSwc3sDCx46UU7ep+", + "dudz39ZbX9KsNefBaujrkiNAFrMBveJpOeRKnZfPlin9HeY2ooFQzMATVg7ejhnWXbWM3q2e4j9ZopQT", + "qLyqBUA3lisK1ozOFKmn/F/3mWkJXe86buoWJM1HiuvUF0rYtSJltDTtsReySZic6tL6LJ/Nsb1u0uhm", + "qc2pPqpsQfcw1yd+Fo+7DspwB0lh0f+Rc+07IyCr9+optcwibYIenZ7FX8tqlpwy48l3SDJSk72GWoPh", + "aHcJ+IJzH/ixOdd8Bha06V/LU//OVLL8nUY2CjajJTto5rlWc5G2RBohKc8cz9ikyqwyrI/dTqr5ZLvh", + "J5pPlkfP1By2G/1WzWF5NMYHODaxafC5+/BnWNTGko1u08BL/Ko+DOwgKbRRGzX/S7DH+GF9dAawUWO8", + "dB95FK7FJK1GxAVvxQqGNeRwDb6N+6aZQ1ui6irLq2nAtnHycJAY564m3XBMJyeu4M6W17NM5fE+C93O", + "sQZu4QRbbSi9uJ/wnKkU1mgaaZiduQ/ZnkowFgRP2WUYM/mvP/yw32cnJCxQFvzrDz+gEsetewV1Xnb+", + "n78d9v71tz++7774+E/xpFg7jSQXjIzKHLepNuE+RHsEHn1pkYP+P292gbqVYpd5AhlYOOd2er973HCE", + "sPEUl3n8jV9AgrJvcr/dx9ydZyvJOzosUjsJO8ryKZfFDLRImNJsusinIJfhz3u/H/X+etj7c++3f/mn", + "7eq5nJD6ueU7b6kIHFqk2gVuUO3pu6qcTUvlHmz5PdDcwuYp/ddMY4NxyX76ne3N+MKJH1lkGRNjtMuk", + "YCHBYJD96KK3Io0h1PJq+Nna/UevdlkCPY3C7dhmi7JdKtmkdUdjecE9Pup66OGyqnLiPlmpajgCewsg", + "w0acou0j8Lm2Hnsd/2c8U2Xis8VSFTMhxcxt9DAGk7U9uH3KG4bRsfDlyt6Cs9SRlga6IbeXWRlCb2ZK", + "2em/k50cLbVo0g2mOqdxuzOMuPEF+3FB5C8ZyIk/B7+jczw7PDw8rJ3rh+jBHvLKcEfY6ZER55TvNdZX", + "YpkwqFb+7a7LFr/VVfqcC21K2IUWGNRPwG1igjFbb52q53VHxi3LgBvLnlOXfjTzlDtd3nI9ILIMl3qO", + "l1f9x/Jp1v5IsGzgsINrxKPKpsWMy14mboD9CL8LrE2r51BhM0L4li/oIExIY4Fjo5VMSODeJZurzJuW", + "fsX4IrcaGgnMIAc9MDBBTCNygHyARDaYGbRpi4lUzRpXtYjzxueNI/2wI12WRXdwXysQPKNdrFLDRvpc", + "OWfzFXvY/owtt4S4RfvCAqj+vnyoKrKJ9g2yt7Q99qyx12ebg3jahHtphtvWILY08Tqzyym95c4zvrhF", + "LrytMIh3oKu9DqspMcktEl+ftthLqCXNwX/wOad/UpZcNTc9M/GPU24YTzDUyyr2HTYr6bLvfGb8d/S6", + "/M67J75jc66FE7f+6TjLM3jJrjv8lgtLDR8myqq976bW5ublwQHQN/1Ezb7bf8U02EJLVvscc3r39l9d", + "d2Khd1SMjYpyJA08/NMKHr4lbu3PiE8YMauXsSrVayYM+9Nhg8N/3+Dvm3ENL39LfDC44R3RIbRMXMKC", + "6nSrpvaA5Uv5LNgg2KOw05uq+/G9meOtlvymV9+JFJVPkKziAHFze5R+vk9sJAUd2c9lCGKlzsVl/GL9", + "YBFLbqpiRYjLyXzA0pazFYjw62ICoH7bkDI/BDvjxb0BjaQ1v0AMQV6LDM7kWK3yI2EGqdDrd4XyC931", + "5XOupZWmai3u6UT5DBUSH8obaq6VKU0pt9DztX9X802ifMcdi163I2F9ZnqXXXdSfXune+7/rjvuYXPd", + "6enbnu65/7vuxONG49GpP3IDjeTDsQjBB6s3sfWrOOisq0gifofBaGEhgieXPuwUf+77OqJhGwLMFhGn", + "IXqYo15fW6wb8KAGQ3/pbehEocUtyY6vSx8NI99jWyfnbdCPo0sNe/tsiYf3hWW51H2BuhuWxM1iPhdw", + "kUPdBnZ8cXp0ddrpdn69OMP/PTl9c4r/uDh9d/T2dIu8Pkrpa1VYsP3cir89Dt8T4f4r5KwW0teuL8v9", + "lG5VH0cXeox4vv0zxc1jKGSVdsLLxDWeMcvvlFSzxUtMaqXiEb7HWTW7sRr4zKcJDLFpGTrYlJ6hZqFk", + "CWvUIdxWRpCpW7ZHFm7aEpm+fUTSsP0ehl2mYcJ1is59DANQLC9GmcB8ZGH77JhnGehe9Ud/ARiY9P7y", + "ih2Uuz/wP4Vs2jJ1MTighaGbfcUMABsu7aV8j2IzYjPlOfTZLzwTadlKIMHNhJyUesyqMOUFh4SfxBcq", + "wr51GIAZPKKoI6UVxEngz3ieOzRzOkYuBm6tDfEDR7lw10Mo1Q1R2AOMkR4E4b92Bh9WfelGkLZSTpbm", + "Ax+mtGmOND+mD+tj3fG2HX5SflvOQLFIA68NrZ+AvkUNaXl8pibbjX6jJmFsLd6JHIAbZjirvkdnSGwe", + "dEdsO8vPsIjNQRb4shrZ1tORu6JRYa/bycQcBnMBt1sC+Y2Ywy8CbpcgXU2zNbzDTKtA9+Fctak2HvMt", + "DTmpjVieTUhhB15H3mqyMynsa/x+eSoNfpWd5rsIozZMuvN8q3PVQ6a3meqy/D7MVC9huF0v57M0g+XR", + "jjsKOdnumvw8b2hM85KW2spu210aP16dwzdR3r1FdafbbPi4WxvNMDrSPe6effrCjEu9pLZumNTkBaut", + "gXbvvFROk+Q79OEoRyme7lLwPIyrFe3duSDy6hw73GNL5dLuStm6XSsC1lLEtkO2WGmy7kqNil3Lf/j0", + "bfeuWLxD3Z/U24/djpKwfTrDsnT92N1lWE2kbzkwxgF2HVqn+93GRljYbhNUvHTLcTG83mFonLnsMEFF", + "kTsMWsL4HUY2sHyXbS5zq13GBl61+3p11nAvgN5nhrg6uvvgUgvdfWhE49xykha9ZLfRq9rgbuNXFKx7", + "Dr8HH2hRQbcc3XgPbstql15v2w9bVuC3HBl9Sew49p5Lt712txweFZP3rXhJ3S3eCGPRtBcxg2nNF0yN", + "I0Y1IcnGiwmnVDCjv21hjNJwHfFGl2I6Uts0U5PlWgU8zzNvfF4bp77clWpS+jEs3NnWLkIt3U6uxMz3", + "4it3RL0KKR9/Wwt4i3OwvnTMpodhHec+hvaifFEsOwG2De4NoXP3D+ptm2HrYN6VGMrd4l8eMQ4Egwof", + "GAGSCmO5TKDhFvzhqeM+3J53ivt4eDCEt91XkQ/un1zapVuMm/M3oWcVWBIwjFl1LzTddqad0PX+kYkp", + "GDvYFGFZSyEKfqVNAYrdjtHJpomp4N/Wcy57I8MC3dopYjf0/qbOl3ZwV/+FCuez9z8HgzWs8nV1sxFr", + "z6iRBpjgb+1v9rWqm+hZzrlNpj748X4Qb4t+PGmPeiwZxfMXh7vHQJ60xj722dk4JBl2WWF8duFUTKZg", + "bFVdmIYErqgB0ccLWe+9+tNh9/vD7vMfus8Of4tvEa/Wm/E2wWvsY6M0jB3voNQw8TsQCy6rlzglpAx7", + "PdCAxxSGUSpenNP4HLMq02o1tLZanUrghvw7XyS3On/wfFrFQJqC6h7zlOcUaS3hNlQorAJEECfwLqfA", + "03GRdSlPOvwla0HP1qDTk9Zg0xJtvn9+uF3o6XIGwv0k74aw0CB1g9iiyikLQ7Ggy40WayjqwH3YpW+5", + "BmaxTNvmyLM1grQMpZ9tkqg3sKBKj8y4y/ESfXsBG1//jQ+odLObxWykqJgMLtRnpzyZMrdEaBsyAsZr", + "3zJT5FVVwrtUWaWya7lnANh/PnuGZ1nMWApjLOGvpNnvMx9eZcpqmdedCwy6ue502XUHbRL0z2OrM/rX", + "Ueb/9PqH607/moIqKe5OGIoKTXCDPDPK7TJRs5EXWcZnItB8/2JDvAb+F672L1d8hNPucKFL3BpvN8qv", + "qRbY6R0kjxZBx93xZhiluZCOj0gsV74qmrieNIMx/xapjEQzcT0pyn6q22MVNwOtVDOUMn6Moln/G+sc", + "uKEs12IuMphAC9vhZlD4EgLrpwxtB93XbipZZCg9Ao9fzc+ks0fiI/CiQxEQM4UsK6/cyYIi3r0tuY0l", + "hCuNRcqrx+oer8dz7PsZvYecFqF2vMsH2KxzgZy3o9cfsSh6D7M/Pi4D7FTOhVYSHx5ldCRWn/Ztk+KV", + "7irMX4lw3C2osR2A7bGLBM6NZPigwEVeJ7oSYOU5Volw7XvwtDx/22MwXkUQ7oQdxCNlz0MdxdBGoqUo", + "PsYxDkZ/ehEPY6qVfqJP2agYj1v681Ec47aTqcK2T/axHXo/iyrJcDfwXVITDcReWRZfqGFvE2RUn6bB", + "1DpXpxdvO+vnrQdT+c9/PnvzptPtnL276nQ7P3043xxD5ddeg8QXqIreV5pQpVt2fvVfvRFPbpoli5cj", + "sTMT73tZdtFJVFbMqInkuijjbker201zuU92DI3HWbu00TU3dpnzW1m/sK0KkEVE92onYl85FgbWLjZL", + "wSP/NeMsN1Ckqleefu/86r/2lxkrafYoiMrAlzmQRGoRl3GghSZRy4DzFWJqh8BgreWEih1AurKS++z+", + "y3yM9kBuwvUe/PysZjDmI8eQODNutnX0EK37+v6yBFZb/5FQWTc2/BILNPbKTrGRPmW1/ZR23KIQaZwR", + "Y1vnAbdxOzH1fljpxuKH7WAqbiU1y21hdq2FVyvBVhiSsu1cKS8GeRJrimqsmGG06PH5B1agPT0HnYC0", + "fBJrvr1WjFZdmESzcvCUG9/HbBsdhcrnt8RbVzsOxchDLXTafRmK3SLBo+aW8wqmthHfW3X4oe3HZVE7", + "YFMh7yd0TrjljpPdakEG0CXUo1QHIfMiEr6dcsu3UizS+iqbG/CU8/628cwP0hfddnxaqXHTrZ7QfWFB", + "tiFJlYeGHzD/eb+zrUnFH0UDr2Lpd9GdLk/LmvMacg3GcahawzGfo6L0SnHTh0KzdKxVyOJOEVVBIe6n", + "e9Pc0krQuyOFaILxVqyhZKQ0uTDsGgded9pI1u0/IgXIEO6DzVWtDVAyLeRNs24TpgyViUhbEjFFiyP8", + "H2aHGKl0QW1zacpQfZMuQHrqXg6gX9/pP5adUJY+ZaWNDO0U6VwYpRcvfS3LG6luw+q+vkzob1c2YF+q", + "TMoLO1VaWMzpzKggNSW3mlp50T47o1p72ErS+EJehaQFk8JYh5uLHEzXoQHZXrHuF/GYZhucUOK6qnTc", + "DSXR63WZq1rTjerdZfHcRg3gMr69indd2/aqrUQoXZ4n8f6De1xtyDhZ3+p92+omVNECdDzjbCwkpkZs", + "owZVZSvCqDYlaKM9ifS71T+bsv5G7fdG8vTWSlu1Wz/onptdumdUJuv7jN15FYN0AZNtKkdt53f6ydeH", + "DVVEJt4IsqbmRosn4lf0QOwy0ZZRCTTXd+45lvcyGDvuryU8KE5hhzmjruBwC91wsZtAdh+Pii4BvaH8", + "UxMxoiKoWSRqVy91Zvngbr1j5yelxe9KYgkiXIvxmSqk7TMKT3EPZ/y7YZh43GUSJrzxdweHuOSmHWyo", + "OPKL23GyxfqpupWR5Ys8vvhDIjHKMlXbG/U3UQW3vjBnVUurudTuRLHzlFuHR6wUGNuRa4k0BbkhpZrC", + "OCofmR+00cfvv2vZ9muRwTnomTBYc/N++8eugXHDGzUUpGxVzf7SsF7smhYdqfz1pxcv9ncr9KVuZczP", + "4/aKP6FnJ+z3Q8t+t0mhpWzOvLpbcueS59AXMr5nEa41Kc31inU7dqbhhYF6gQPqFpBD4mg/LX0HOzof", + "6p5wLFUX8z3US0k0gsYONxJlffHohTgV5rX5ldvkUeuqlUXv0ByA9SfjxSAc4Yo5bLbbltTu52Pl2Gyx", + "RSxPa2QS3sADq7ONNZ9BPPLmotJtw0cOxOPcUewctBYpmPBW8jewX4f588NNRuCoSTQ82CLGzJoCC0h7", + "j1QjDjcdEPpMXhICtzseq33UHW8hAHP97ay9kBm/w9oF4nc4k29/bN8BRjGHRl9vf9wSIsslu561BGS5", + "0x0VqVCbkfvYt2/g7nMqe4ZtoeciBdVnF4TIpv6udnoGn4N7+NMoH8nnntLnRWbgyP81uQFbL3KOjfAw", + "KZ8ZsIaNlJ3Wapzv+8pUFKTUxBdhaEc99yLvb1m6/NKq/KH0pXQCbp7NN3k2m0EquIVsgR3dMcpBFZZN", + "NE9gXGTMTAvrlD9fkmCGYXFoKhQS4zq0LrBBBB4VcSTu5tmlJiMBzG3oCQsyVrUD5Bwyle8aa3mFde9o", + "KCvdIRa75taK1LClugeR0vzBELi2rGqz+gSWrP1Hqy29N1NSWSVFUgZfMXIiVDvliVaGiCQTY6j3K/VN", + "uNgH47v+vuHG9nDl3tmJjy4sfBD/5eVpsAN686cwVJ+OLEor/bl3cJe6MwZL6W9rYdiW9LBUdoMKbt0K", + "Db0M5pB5WxKWisDyW3mtJIeHHAOZ4nmQW4SyHb7wRnX6PjvSI2E116F6hlcvqXOTL8VRFZ5wDCylyfrs", + "9UqLvnX1Qbqxwh64Y9A9tFkR2rBUJRgkBWW36KE3gv2zr5hxsPSXE5y3FgDXZatlQTa1tl9rHv2ijYwV", + "CP/j8v270sYYg08mjL/X9eVRqFoUuSOW4dWsFB6DBAHSXfjTtfK/BBuwzIvL0k3Q2tnfOl7vu+GW3f23", + "b+6Pnfwbvf0bbf0b9Zfr/VuQS+HufIhrP8ZhIv12P4mRt4T9ZfB03sOn3NYwaTVYMs8z0WKF/ZVnWS/J", + "VHJDV1aZK2qX2Wy15eDrp6Q8HRuKQjY6wtS7MG3vgO/6xiE791UquyndU9x5iZZxY1ckcdUHX4MBGyRi", + "81roeW36ayXZ4RaxfngQOkcUd5bqpu9sb3xYdeEbWBir1Q2YaEXQaPRLvGrpvfKiQsBmtY+QF1bLj3Kc", + "6A6bmmd80b+WDSahC2B7oZv1LGTEHaShNvR+n11Sz74yoeBa+ghwxwLcWqjrcMlUeA/W1mvcFNvDv/37", + "obsXn7a137+WtSq12PrC3doiJylxq3SKbdtT8pH6kOLy5EJazXvuK1rQXEunN0hOxb9QINLPOS+Mg9MV", + "tq9zeyMO7fayBnTR9kndll4eDhXxXrEZAQmDqcKwdWqj0VK8TQ0cwSSwHhexc/GUOwHvdP1FrpiQf/et", + "/dyr/xWbCWP5DZCihHISdRC8sxFPbkzOE6iQgB322XuZLTwLM7EbYHtGZCBttmjc07WsPkPc2KerKp+w", + "h/1nUawPYTnb9jH5VQsLZeeV+xH6emg1AlZCscGw4H0bsHzEXrzktsQqsZ2XHa+NnmHjXnZ0ftbpduag", + "DW3nsP+sf4gG0hwkz0XnZef7/mH/e19qDw9yEPKJDqgLExnHkoh17C3oCWBuEH5JKAB3wmBQh5JguqzI", + "nfBhS5NGMpLmwj3PctDolU+7RGRYBreQVmTU+zl8fQLzK6Uyw647qO5JISfXHcxbzoTEtllqhDpTykYw", + "VjrUY0V7kU+dQ2Qqu0mepWgftck0rPLad6HyFZJ+VOmCglmrzjxVmvbB3w1ZY0liRlzJ4TaXtItwJLpD", + "q9gMr9XXB/3bdafXuxHK3FDaSq/nO4n2Jnlx3flt//6ZJrShOFpV3zn6pGQzzFrEdZ4fHkYM+bh/gneK", + "L6vyaB7Yy1ViP3Y7L2immOZRrnjwIw80SXWqP3Y7P2wzDstmSJ75UVjXdjbj7inU+UB4WW4x44VMph4I", + "bvN+zziswt6yh9kmqigM6F7oA1QtA1g8XQsDjPrBscpWV4a8jHj5c99hVfdabiQXtju1XMtdyeUYNNa7", + "D7cQuh67R8qNfwbLseahNKbHYnYa2r1d+jaI3WuJLdx7WBAd0nJGOkc5f0BDNPoen5wfhOx07JeqgY2c", + "Jg3ptUQLR7jLjZR9XrWiuy9xx0VDTKPaBvh99nPIBfQ/ST4Dcy33fMaZl6bHSt0IMP4erzvUNRYLTnvX", + "07Scgf7av5aXACyUG6defNVO+hOlJhmUiH1ALqEyXzb83QchUcadO/+P3IjkqLDT93PQP1mbn4ZGlnQH", + "0Q2jacl9bD7kE81TMOUoL1Tf8rvj0pJgzkGfOzzpvPz+ebdzrvIiN0dZpm4hfa30B50ZdH6ullLv/Pbx", + "sfhawJWvlrUto507SzuHK/JM8bRXdWjscZn2wreO7SkTUXQ+4DAqYqvZzHGQcgr2u8gZ18lUzB2Fw53F", + "9oh2CjNWyBQ0O5iqGRwQC6k6ZJqD6+Lw8PvEkQL+C7rX0r0HteNxs/oKxLeFvIeiUXLOa/kJFQ26r5Ix", + "miOZXvg7XseTZkVmRY6dRZWe9YKtrE3nqPXZbE3Yrb5xygeBnwIkEyvm3DaqbzSnj5eufq0yB1N0r1vF", + "8own4EvOB3DtBvUll8JR76+89/th78/9Qe+3P551n//wQzwK4HeRD7B96MoW/1ohZGji4qNPC5lTLlNF", + "PuWu97C/X0g2nnEpxmAsiuj9uhViJKSjxE1afbk9XwM89jJZq8DVoHs/Le5ZLCK5xAZCBUi7EW5HVFMS", + "B0ar8vRz870VFlRCs4bke9w4hmT260ywPKLnhv4tfTAKOl6c652GPGrJ1FJjoaWulobccr7l5dH5GRa8", + "7rMj/ytKfgpXcuoMWcus8B3TRQYYjhVCpO+SrDAOeZ3602VGMamYwsACTH6oeuQblnBJNgrsuo1dSUL0", + "h7EqN8GIMBbaWN9zIjTMDBfPRFl3hKyVoREmNQO+lqEsemHQOYmdiqeeqlKgDC73LqzsgJicQwV13Go3", + "sKDOpP66rmXweOZ84WbxjgimVSHTntUiZ051lAnFkAMWGJCpmIu04JmfJsZ5f0RFsNm59P5q4Fqb6epK", + "VfPF+ykjOGVL043PSXslIVCX1igB1HF6icyWmqIGYmsCrmqH+kTwivRbvSeYqENd6CYbyPqzQuhSzIqM", + "EkaJ6ur9ouOGxBUYkbnqwLH6djBdAE+Pa6at2G09FriarZIRWktvr7LjsV8S5dQK3Tz4dt2hybJcZhqt", + "WPnarhNtg+332TROPhHqxy2g90V/tHr67DJso1pC4YthWL+SQTYY07eAV9mEOA6mMjr4iSC02t54a+A8", + "yvq10mcxOqPA5bkIjTjK1/IXA/GfROqLsKjben3HJpib7bXjWh/WlkKtBUPkA0OlPqDd0knlNDceqiq6", + "ZbUlrxCGHsjl3qATMQ/tF0kxzYAbQN2q3tVqQ+PKmMZTtmF9ItRcbTR+T77hJvpCxCVupaqcSWDiCIcl", + "jJmAJYQZlP3/W5nEX8A2qpw+pXiMl1ON0y5GHdBJy0M8xi3+BWwjsMFrHsQswkrbKB/NvvXxyy2rrT4R", + "mq92xH+QduhvwZ3s86L621BEtAGdIBXL1ICK05htIFY1uF/PR0M4cLkOuvGRZ9b8/WVeAtnJqwSZWrm5", + "axkrIkchYljoLNcwBUnv5tVqdV1mAK6l20y84hzjtjKjT4TtjzVACubGqryv9OTgzv2/XCurDu6ePaN/", + "5BkX8oAmS2HcnxI/9+FcUyWVNvXADx/9GM7rXtQ+6j7xV4H5Fcab0AgKKo16PHwJxCcih+UKi/elBgQo", + "YsuXpC2QjK/bkhAvt0D8eqOgNlZ1xW/gsh4a+SQa40rK5kcPo7USBwNZD3JKMa5W2mzdXBEs1QYoOvaz", + "ArTMTWAVgEIQ2gZwqixrZ2KUjMrmPmGTqgAcKEfbIYnU/c3WdLwaJ21qiw07X6OOp1cDG9mgvpm2ZJma", + "YK6oFcmNYXtSWZ+pTCbOGgaxEUz5XDiU5gs253rxitkCrXQzjKSqFx3AmClMq6iOQu7GkJyKqazeduld", + "3d1G0QQf8oOenoZJc6+cA1XhaoF9ivtAKxIFC4VY8MAKhyE2jAwYvZ6GHLhl71ivR0FXh4w8CKSQkw9h", + "GOOQlyEn9InIr5alfF/u6NHrC7Eh0WYqXYHAw63TjHfQ5kLQbwtz9AGXTwSX5XjOBxk5KIjwi5Fa7mxk", + "1FgHBR8j3M7TqlrCwd3I3P+jMOTFcngycq3SRWQsdwqaVXmOyRgJsD0KSOheS++TrbwxXcc4MH/Nu+O6", + "NZ3Pl4M24nchJ/v+1VwuJMpiYwzueGKzxbXE5RqeKQ08FdLJcvd6du9xjKIOawyphHahsyGu59kOZyMw", + "tgfjsdL2WlZ9zMrC2WHW4KVwM6Oi5h42fAKM0hN+dLzRASG0TtUznmGoqVXXchjUyaFvwMDlAm+aLVTB", + "UoUh0BLcjo8sy4A7pVUGwzLFZ7iv0S85AuZLKvWv5UUInGnCylinOupClhWP0W31shZ/U4eNh0CX3Otd", + "VI7lMsT6UZBgsRsCB4k+kCkFxpZJOxSzfi2t5tIE9fYlE2PG0bWjq/Aft290NrkNcp05sVgRHcMkQ8B2", + "yCETbsaFdPiAa1MgcAIeV92fpJK953d33t+Va5XziRPI/Wt5rmGMqrW7HifGDOQcM16HVXTBPw8peejA", + "39EQ/Xk+upXIJoPgXexZLSYTcHrStSQYECUJifD0CaxV+H5MWIVbPi7p9xEDBSgsaFAPb1uK77h63fs3", + "n3vTjF1iM56z//O//jfDGG8DMy6tSLCI8vnR1fFPbDV6Ll7z2H81aAmUrO2AfNxs+Mc1BTFed17W4yR/", + "+zjcckM4OrobD9ZttjFzTAM1k/g7abXPwpDtYcmVAyq4cgA26YeEVao3HgKqVxGIQspNN/hnMe23TBBZ", + "5saiYsWNsKUGpTaJNFoSbU0cyWk9zMegFTLsPnESKymwMkk1RR8jQ+gYVWbA2rij/f7mIJQHh4g8ffwG", + "xoy7IQPPO1dv03Ld/93YWHQKpn2BwesdNmJnMNjUpzF65uxZgekzz85C/JUvWYEF032Dqypw0A92/88c", + "1Nr1owZvIHPj99DdTqF2bOjD/A5oFXTsD/cpQXXo7i0fVCQxJKmALJLA7eMZwmHtlJfxNcbJO/zgVvM8", + "h6pJl1hK+mkDly+H5YR7hIwv3pTeHy/ewQv3iguvFd+lLajLMpATss8nnGjNsueHL/6N6ix2K9JzAEww", + "2JfCKJBHeADQLkYZtNTFbt7lGqWtSrAKN4jeg2os5XBrkZOzcgknS6zYczKyrCzkM4mwNj7cEUVuTOb+", + "olxUDU3I88tXlbpZYoGbOYNl31X/IYr9i8M/bx7nNpiJZOU58DjO8mXtITwfWu8JUOFy/4u8vIzpTlk+", + "5XjF9ZfHEeoz9GxPS4UGn/I+O7epieZZYVbuPlT8OqhJ3zLKPhLO7aXqUxk4Iw2SPjFG+9VDsuUqsD54", + "L2t4KzUu+bNh7INjl1uO41BjbA4SDdzCoOyDgWhSxCKG8MOyiM9ThQ01V9kJVZ6tqzlE5/yCzAt0UsYx", + "56u6/gCXFBzb3AIuJ/jhU8OFVqk3tLu3X7oECR0xfRhlvdg87p2yr1Uh00d0aOPOGW+HW9CD14DsNam7", + "Xza0sKLc/wBAITxKGKlb6TRmR12D3wWWEJqAjVXqsoWWhnH217NzVr4Fam+I8DQoi8pU1d8CavRXY0j8", + "+idC/1XkGJGv+QwsaIPtL9oaPpaUgzqoVaWu71SDcCh83blx/ygA2QG96UIdvCYOdOtGjE119X7bSTj7", + "e32Q08vdejhjWTsJEat+wV8jXnpg1VmIew0QooUHbRxfjU23QNjw9t2zXNcewLPgHEY91M21vxavr+Ua", + "xGZ/NTZlajwGbZgREynGIuGYej7mhp5/tKDXX69lCvU/uX9zTS/A30XuDS48mQqYY7tcsMuzIBnFI7Nq", + "VOXu6Gshq+4fq83fyuNiBEOf/SQmU9D0X2UPaWZmPMvq5ohRYZnlN8AyJSeg+9eyR5Aw9iX7bwdtmoI9", + "6zKf+O8ACynb++/vDw97Pxwesrc/Hph9N9AXNmgO/L7LRjzjMnGqlBt5gBBge//97IfaWAJcc+i/dgM8", + "w5AfDnv/1hi0ss1nXfxrOeL5Ye9FOaIFIjVsGeA0nTo4qtZR4V9VpSZ/VZ1u7TfaMv7DxFoS7MoVPfU+", + "iC1eLdm1/j/CGpfMeSV7RINLqN3g2WKTNZTN5LflCcgJ/LWu9LX/UiTsbjph1VB/FaFQy6t16/8K0eYv", + "YOsnKNtHrUCvRJtMGIt6umnFmzfCYL1nc09h8nViSnXqCKpUz7eMapN8hbiC2boIeUokXMUNbJTf9nwL", + "rd2fMDT2MZ5uGIpamTu+QjjhCbCZN3q51hGzBp6Wj+4oLV8AT/2TeztSxsWCSujm/1KoWSUWbK9qWvQg", + "XQJZfzSP6ytDFswaa7jrSuQwQIx+UKst30rdqyX+ny4JqaWXwL2ra9RK5/uUoa8QkJdgVwm93hbgANsO", + "mKnISwiTB7Q9CAvrnJiao9TnjitdxZeQQPCh+hpmyvMAymXrt1SdCOrBo0WPlBpJi4s+BWMHG9opuG98", + "n/WSg/mqaV6h3aaRQrdzX2++9+RXW925HAPdwqNVYkAolUUYvnZWFynOMPb6Wp0cgmlzbZEZjoYXikHD", + "jtlUT0ZYU9k2V9JXlvGrjTjIuvlopLEr6qf1jhO1SjlVjITajg4eKbJlHT3cE7H/KvIKrWsA/B+D5Lxe", + "8GgJRVfw3RtXNiD8rqbRNrq4lpsJY7OJtGERvZZLJtH2ckfexvloxNUaRXU1hWXTSylCtogb+mxEG4/y", + "aSvW+m77QB/fxsvvDYsZYXlfh069Hn7Tq8bt93eroRzg8CTs4sjf4f9wlrGMri1s43a5INHSS6DWCOmp", + "3gCRXkvbw/aexVPx2NG25x+k+EcBsQZBFVXe+uvYKl5tuV67TabssWv8fSZko8PUjdS+UJOc1DQxvK2D", + "P8KVf/RlzIGKlCzjm8ordFsyUqDhwVsavN2hhOM628NmU8OLWGF9AhQFO3/lgLrElj8hrjxm7VsG0gHl", + "yLWakqhr92tzSp99Qlgtm4Us3FnabdQetMkfcIlPW99sJ5JzWjW9UePaW9jnEGKTU57iqf/o/Gfv8vK0", + "58sH9a6irSjeQiq4r7Y+xq4y2HrDpyTuLTOx/YbnLnjpVlhdxCn38WtEU+outHzLvuQJsd0SY91jfn2Q", + "ERbl2cbgeVJTvviK8fMT+r3fVw0JQhvL1g6Wjd4pf3rxom2b2PaxZVtr+14S8W0j8R9ojr2nNaMsCfW1", + "i1E0SznJGeIhq1CtTE3MQXWxcRedmhginRY+vIQQvrvQOswNjMajeFXfNtrVP77MWGWZuo1HHjRaf9f6", + "5C2DGRM8yrQ9MQ7t/IRhfmtrCLNdquyyTu3s8dWqDwY5tanpfDaJ9kZNthRlDrG+aOkVkwxu05RDeXl5", + "SgSSZ3xxqyntjYpGblFetWz+dV6OZoljtugLHWsw01pTWwTNnWV8woU09BIPWQi6kFjCWSrJMpXwbKqM", + "ffnn58+fU3YqzjrlBnvOGWTV3+V8At912Xd+3u8ooec7P+V3ZaeYUKXB92H0sRg4Y7U5LJVrCy2r1m8B", + "vWKGE38F1bmPSTo8xctuZa3PlPUQ2Ye70HiySnm5X2I51OoIWHbgEndOGBFBTk8gxJOQOtof+r7Bllvo", + "yer7lCt8Jjxo7KANA6pqxtp/80WUwU3UbOa4hFnIZKqVVIUJVW8DgE3Ob+VGCF/iV08KYlzi88LYb6EN", + "yPjzZy5+sgpbvga4f/h/4Nv8RjQrCEUB/bPAUjSb3+XVzGtVwlKTLwqRPuSxcC+AutN8kZVK3//8VcYX", + "OFYiJu6laRULams7xlFhgI04d0Gf/Y/BOjrPN7x7vAAlrC/B2fnVf/VG1EphM/IZy23RbooMLJ+++tS4", + "98RyjA4VE2H+l68yStkDgJlwvHbQp2ILnQa/+h/DdfA4n1l/oi206U8/LrB1B5nfvlqLWyX5GOHZWjxU", + "hd1kiKsuTxV2rUXuM/GjB1iWyrO5YVvamMLtqsLmBXXVz8QYkkWSwTcHytM5UGpYrQq7ZDDTkGC50MlB", + "5YSNc1fKHL4I3z9pona5yubassvpnn7g50vR/ky1LcrE7lzDXOCbkRFwIWVzkYKq+RFqUPfJZa1cLGSf", + "1QG/1ntWOq386rreZJ+qkPkm/o1qrkWo1e29AuXwNkcWMr24G4v3fj/q/fWw9+feb//yT/dijXhhB7P8", + "xYPTCSqM9DGPDQZX/tp7LSQ2qe8dxRo9ixkYy2e5Y3LUnB8tu9XUNLjP/lJwzaUFipcbAbt4ffz999//", + "ub/eA9LYyiXFo9xrJz6W5b4bcVt5fvh8HWFjcTmRZUxgsciJBmO6LMd+FszqBdk+qcZj87ovwOpF72js", + "flgthVtMJpQrim01sAOkkKxqmB+6L+oFEUF1iDKW7Vkklu3jV5xwSqV4DdIiNVDfgqNkgqRHa/7ghSds", + "89D+FGU+wDqBElajTM+VIPsVeg2NK3W5y0dLsONZVp+2eW0rHVAjoXdPLXybi6yVvc/WkahnAl9hhSi8", + "gbKKe8XX+uw9lZyt87ocNDs7wRaIWNt8IozFLo1YstpxkP4qlFW+Dsgqf3oY19a4v3rlQ+E+b8Fwq/Km", + "+KHrNgnPwKrfQasD389+bZsQeiu4iX55S0UL3QxY+EMxN0vXAZfrNMPny5j9dHV1zqzm47FImJJM2D47", + "5lkWaoUcnZ9RiWxh3JS3Tlrd8htgwrIRJLwwwD5IcaP52NKvofN44hs73YBvUrIIRQxCzskvb6OlPuiY", + "l+7kV+qvoFVnm7BG/L5nVc+dkvm7Sh8FOGcpzHJlSWz4mfFeIdxq7Yr6q4ADuR5uF2Cs0mB82UyaujxK", + "2YmgWqPr+K+6RRUCb7O5GdIaUKMRaQYEUBpbqjm/vGVS+VIiWDnbeN1mClnKuANb1MsuHw4bkE8EGpp4", + "E2QsZDBzus/GQjv1hkzlqGapvT4LH784fMHEuPYdVe2uiqRGW8/8BexVuZ8ntH6Vi1xabqNm96v4Ae+r", + "u612t2qfv6xcucTOuPZNMCjflQDSCgiUagm3MKFKvHDnLks4xDBYP6JeR4WNVLrAarIU1J2+Ci+5+hQa", + "LKdxQpeYYKhDv9kJ9Mz39Wcwh/rWHb76BSknhmjjJcMu/yzJgGsTijbVThvrYuRusYlMT9CplwIwymXq", + "BTc/nS333tj8FWdO+4Kf68ioiHXdAbuBbgIWPz981sTiW05oXLPCVBj9ygdnuXGHbpywboBD9AySEMCl", + "ctsT8iXjleifcuux3M1ep7Y9vlQCl5L1pLIY3O+I2CkOuoAuUzpQUiCeIPH3W4nmFQkC93+l1PACcTem", + "fV7Yz0dnXzxd7Zq/9DQbMvB5A5wu14m8hhpSS0iJq4dn8u/Yr4NL8gUwEXJTqwXINdFlE+6b+mFSZYIF", + "yZe3USf5Q6Ix/NoYMZGQMpBzyFQOlbLolzWMp8Hc+fzwReT3scjo2bgnVVg+lLj2yWb47XemIlxhKtpF", + "wn5xeOi0tjnPRMrL7vktzT7Oi1EmTCX3yIHzRF5MWguX+ExezOqcHkjRSD8ER067day6hGjCdehSUMGb", + "+o0l0CfqjejvNCFPEsgRvQpbQXo9rr0iCRK28oDa8M2mgzThFiSxTGwrjs5liz9Iah48B9b0+VUzE8H2", + "2SlPpmys+YwCobH8htIzNhTpS/aHgX98vL6WKbf8JfsjgKDn4O3+fn0th05a0t37XgRli7gEjOnNlFRW", + "SZGggzEHbdD0lmhlzBK786mJrxhnb7ixPYRY7+yEbADYLclLcTdQVhIaqQwf6BpMMQvPfjp2n51oldOm", + "KKiKAD7huQkK9VCkQ+pRgh2JvA0DxBxS+k0YqmJhp1yyZ4xPgach5DtzezUAEj/tBl/nLWjHKATmLZd9", + "4kfFeAy6z44zgV/53qZW8+QmMptTFlKwkFjcb5+9xuj36vgm6BdLV4Ymv2rZSu/3oHLAwLQKA4AFvmnX", + "r1jOjWHD/0tDnvHFv/MsG1JeeWM6laVY9BKfFo7bevw1Frhv/HQr3H1PeY5pGtgOESRokbBhk88NqWdr", + "0Jr87YF/yHjK/Blbn1BjSbbnPl9gCyaHbdQokLNUJcUMpBs1tIschtRErGTWQ+qZ4nBO6VlZNKRq6OP1", + "lX/GbZ3gx8SyusygQkj7ocmjHQYR4ZrH21iZ78KhbOhGgsqdadKT7xamNDMgU3YYgUcAb2jLty1NdplR", + "TcKa86ygnIYZODLTGhKsBUFLcbeGkLbPrvgNYCfXBFJcCP3YQ8KbIYlVbCdJC2OrMlzOMSReWNXT4NG4", + "Wi4DLrFRFiISmf17NKWD0FQYLG9ZVVYlb1LlhGwQwW5JRueI+LsgfJ9dYA1gJGmWOH7CLXt2+PzFKxxQ", + "IjOvcQIMEy/0mCdARUPHQhtLxD7BDDPtuUy/tYAs3Ug8dCLL7lcD9gHBJ1vJ8zdbCKOvLuNp+QQOopfo", + "P+9dOnr0HMCN/n8DAAD//wL6kKOLyQEA", } // 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..55a9577a 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. diff --git a/server/openapi.yaml b/server/openapi.yaml index 4f56b0e6..33004b05 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1592,6 +1592,30 @@ 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 PATCH requests 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 +2171,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 +3063,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 +3097,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"