railwatch 0.1.3 → 0.2.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +231 -0
- data/README.md +10 -0
- data/app/channels/railwatch/environment_channel.rb +28 -0
- data/app/controllers/concerns/railwatch/telemetry_identity.rb +25 -0
- data/app/controllers/railwatch/alerts_controller.rb +48 -0
- data/app/controllers/railwatch/anomaly_rules_controller.rb +31 -0
- data/app/controllers/railwatch/attachments_controller.rb +22 -0
- data/app/controllers/railwatch/beacon_controller.rb +67 -7
- data/app/controllers/railwatch/broadcasts_controller.rb +15 -0
- data/app/controllers/railwatch/cache_events_controller.rb +30 -0
- data/app/controllers/railwatch/commands_controller.rb +16 -0
- data/app/controllers/railwatch/comments_controller.rb +11 -0
- data/app/controllers/railwatch/dashboard_controller.rb +69 -0
- data/app/controllers/railwatch/deploys_controller.rb +34 -0
- data/app/controllers/railwatch/deprecations_controller.rb +54 -0
- data/app/controllers/railwatch/environment_scoped.rb +99 -0
- data/app/controllers/railwatch/exceptions_controller.rb +53 -0
- data/app/controllers/railwatch/executions_controller.rb +13 -0
- data/app/controllers/railwatch/issues_controller.rb +258 -0
- data/app/controllers/railwatch/jobs_controller.rb +63 -0
- data/app/controllers/railwatch/llm_calls_controller.rb +124 -0
- data/app/controllers/railwatch/logs_controller.rb +40 -0
- data/app/controllers/railwatch/mails_controller.rb +19 -0
- data/app/controllers/railwatch/notifications_controller.rb +11 -0
- data/app/controllers/railwatch/outgoing_requests_controller.rb +19 -0
- data/app/controllers/railwatch/overview_controller.rb +38 -0
- data/app/controllers/railwatch/people_controller.rb +25 -0
- data/app/controllers/railwatch/processes_controller.rb +33 -0
- data/app/controllers/railwatch/profiles_controller.rb +50 -0
- data/app/controllers/railwatch/queries_controller.rb +59 -0
- data/app/controllers/railwatch/releases_controller.rb +75 -0
- data/app/controllers/railwatch/requests_controller.rb +55 -0
- data/app/controllers/railwatch/saved_views_controller.rb +53 -0
- data/app/controllers/railwatch/scheduled_tasks_controller.rb +45 -0
- data/app/controllers/railwatch/spans_controller.rb +47 -0
- data/app/controllers/railwatch/storage_ops_controller.rb +15 -0
- data/app/controllers/railwatch/tenants_controller.rb +44 -0
- data/app/controllers/railwatch/thresholds_controller.rb +40 -0
- data/app/controllers/railwatch/traces_controller.rb +72 -0
- data/app/controllers/railwatch/transactions_controller.rb +21 -0
- data/app/controllers/railwatch/view_renders_controller.rb +28 -0
- data/app/controllers/railwatch/visits_controller.rb +53 -0
- data/app/helpers/railwatch/assets_helper.rb +52 -0
- data/app/jobs/railwatch/anomaly_scan_job.rb +11 -0
- data/app/jobs/railwatch/application_job.rb +7 -0
- data/app/jobs/railwatch/auto_resolve_issues_job.rb +19 -0
- data/app/jobs/railwatch/check_scheduled_tasks_job.rb +113 -0
- data/app/jobs/railwatch/detect_anomalies_job.rb +171 -0
- data/app/jobs/railwatch/detect_performance_issues_job.rb +85 -0
- data/app/jobs/railwatch/group_exceptions_job.rb +91 -0
- data/app/jobs/railwatch/optimize_telemetry_job.rb +23 -0
- data/app/jobs/railwatch/performance_scan_job.rb +11 -0
- data/app/jobs/railwatch/prune_telemetry_job.rb +110 -0
- data/app/jobs/railwatch/release_health_rollup_job.rb +64 -0
- data/app/jobs/railwatch/rollup_catchup_job.rb +21 -0
- data/app/jobs/railwatch/rollup_job.rb +130 -0
- data/app/jobs/railwatch/scheduled_task_scan_job.rb +11 -0
- data/app/models/concerns/railwatch/detection_snapshotting.rb +20 -0
- data/app/models/railwatch/alert.rb +229 -0
- data/app/models/railwatch/alert_rule.rb +121 -0
- data/app/models/railwatch/anomaly_rule.rb +33 -0
- data/app/models/railwatch/application.rb +44 -0
- data/app/models/railwatch/application_record.rb +33 -0
- data/app/models/railwatch/comment.rb +36 -0
- data/app/models/railwatch/deploy.rb +67 -0
- data/app/models/railwatch/environment.rb +54 -0
- data/app/models/railwatch/execution_presenter.rb +185 -0
- data/app/models/railwatch/filter_query.rb +143 -0
- data/app/models/railwatch/followup_receipt.rb +27 -0
- data/app/models/railwatch/ingest/batch.rb +305 -0
- data/app/models/railwatch/ingest/mapper.rb +575 -0
- data/app/models/railwatch/ingest/payload.rb +96 -0
- data/app/models/railwatch/ingest/rollup_absorber.rb +170 -0
- data/app/models/railwatch/ingest/writer.rb +137 -0
- data/app/models/railwatch/issue.rb +277 -0
- data/app/models/railwatch/issue_activity.rb +23 -0
- data/app/models/railwatch/issue_detection_presenter.rb +309 -0
- data/app/models/railwatch/issue_detection_snapshot.rb +77 -0
- data/app/models/railwatch/maintenance_task.rb +53 -0
- data/app/models/railwatch/saved_view.rb +63 -0
- data/app/models/railwatch/telemetry/aggregations.rb +116 -0
- data/app/models/railwatch/telemetry/attachment.rb +31 -0
- data/app/models/railwatch/telemetry/bounded_gzip.rb +101 -0
- data/app/models/railwatch/telemetry/broadcast.rb +13 -0
- data/app/models/railwatch/telemetry/cache_event.rb +16 -0
- data/app/models/railwatch/telemetry/child.rb +53 -0
- data/app/models/railwatch/telemetry/cursor_page.rb +99 -0
- data/app/models/railwatch/telemetry/deprecation.rb +13 -0
- data/app/models/railwatch/telemetry/enqueued_job.rb +13 -0
- data/app/models/railwatch/telemetry/exception.rb +21 -0
- data/app/models/railwatch/telemetry/execution.rb +71 -0
- data/app/models/railwatch/telemetry/health_sample.rb +72 -0
- data/app/models/railwatch/telemetry/ingest_batch.rb +31 -0
- data/app/models/railwatch/telemetry/llm_call.rb +37 -0
- data/app/models/railwatch/telemetry/log.rb +85 -0
- data/app/models/railwatch/telemetry/mail.rb +13 -0
- data/app/models/railwatch/telemetry/n_plus_one.rb +92 -0
- data/app/models/railwatch/telemetry/notification.rb +13 -0
- data/app/models/railwatch/telemetry/outgoing_request.rb +13 -0
- data/app/models/railwatch/telemetry/person.rb +50 -0
- data/app/models/railwatch/telemetry/process.rb +10 -0
- data/app/models/railwatch/telemetry/profile.rb +37 -0
- data/app/models/railwatch/telemetry/query.rb +50 -0
- data/app/models/railwatch/telemetry/query_shape.rb +45 -0
- data/app/models/railwatch/telemetry/release_health.rb +63 -0
- data/app/models/railwatch/telemetry/rollup.rb +78 -0
- data/app/models/railwatch/telemetry/session.rb +24 -0
- data/app/models/railwatch/telemetry/span.rb +19 -0
- data/app/models/railwatch/telemetry/storage_op.rb +13 -0
- data/app/models/railwatch/telemetry/tenant.rb +221 -0
- data/app/models/railwatch/telemetry/transaction.rb +13 -0
- data/app/models/railwatch/telemetry/view_render.rb +13 -0
- data/app/models/railwatch/telemetry/visit.rb +24 -0
- data/app/models/railwatch/telemetry_record.rb +57 -0
- data/app/models/railwatch/threshold.rb +29 -0
- data/app/models/railwatch/user.rb +47 -0
- data/app/models/railwatch/viewer.rb +13 -0
- data/app/views/layouts/railwatch/dashboard.html.erb +25 -0
- data/config/routes.rb +59 -0
- data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +151 -0
- data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +18 -0
- data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +20 -0
- data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +481 -0
- data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +14 -0
- data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +82 -0
- data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +60 -0
- data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +51 -0
- data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +11 -0
- data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +37 -0
- data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +11 -0
- data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +15 -0
- data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +10 -0
- data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +9 -0
- data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +18 -0
- data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +25 -0
- data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +55 -0
- data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +21 -0
- data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +16 -0
- data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +18 -0
- data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +16 -0
- data/docs/configuration.md +26 -0
- data/docs/embedded.md +342 -0
- data/docs/getting-started.md +5 -0
- data/docs/records.md +16 -1
- data/lib/generators/railwatch/install/install_generator.rb +222 -1
- data/lib/generators/railwatch/install/templates/{initializer.rb → initializer.rb.tt} +39 -0
- data/lib/generators/railwatch/install/templates/post-deploy +8 -0
- data/lib/puma/plugin/railwatch.rb +170 -0
- data/lib/railwatch/authentication.rb +83 -0
- data/lib/railwatch/configuration.rb +134 -3
- data/lib/railwatch/dashboard_assets.rb +45 -0
- data/lib/railwatch/embedded.rb +51 -0
- data/lib/railwatch/engine.rb +89 -1
- data/lib/railwatch/ingest_request_body_limit.rb +10 -0
- data/lib/railwatch/json_compat.rb +60 -0
- data/lib/railwatch/maintenance.rb +183 -0
- data/lib/railwatch/patches/runner_command.rb +21 -1
- data/lib/railwatch/record.rb +33 -8
- data/lib/railwatch/reporter.rb +16 -0
- data/lib/railwatch/subscribers/llm.rb +149 -4
- data/lib/railwatch/subscribers/process_info.rb +2 -1
- data/lib/railwatch/transport/local.rb +78 -0
- data/lib/railwatch/transport/socket.rb +183 -0
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch/writer.rb +370 -0
- data/lib/railwatch.rb +38 -1
- data/lib/tasks/railwatch_tasks.rake +128 -0
- data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
- data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
- data/public/railwatch/assets/app-layout-yh-sPWgK.js +1 -0
- data/public/railwatch/assets/app-wordmark-nbjkzxwQ.js +1 -0
- data/public/railwatch/assets/appearance-CDuRvQTB.js +1 -0
- data/public/railwatch/assets/application-C_kpBdnf.css +1 -0
- data/public/railwatch/assets/arrow-up-DVtOGdVA.js +1 -0
- data/public/railwatch/assets/auth-layout-TCwPpS1K.js +1 -0
- data/public/railwatch/assets/badge-Daw4Hvr8.js +1 -0
- data/public/railwatch/assets/braces-DhFbHPsz.js +1 -0
- data/public/railwatch/assets/card-BX_3HXcJ.js +1 -0
- data/public/railwatch/assets/chart-B14-N9g7.js +39 -0
- data/public/railwatch/assets/chart-hover-Vy52H4uD.js +1 -0
- data/public/railwatch/assets/chart-panel-Cb4S_ej_.js +1 -0
- data/public/railwatch/assets/checkbox-D3aRSsBj.js +1 -0
- data/public/railwatch/assets/code-KvW8k7Jr.js +1 -0
- data/public/railwatch/assets/copy-DaQMJWoT.js +1 -0
- data/public/railwatch/assets/copy-block-BKFGd11J.js +1 -0
- data/public/railwatch/assets/copy-id-Djks1fXB.js +1 -0
- data/public/railwatch/assets/cursor-load-more-BXV0f1D_.js +1 -0
- data/public/railwatch/assets/data-table-iv1bdF6u.js +1 -0
- data/public/railwatch/assets/edit-BZc_Iawe.js +1 -0
- data/public/railwatch/assets/edit-DMKUF8Zi.js +8 -0
- data/public/railwatch/assets/edit-__9yJlO3.js +1 -0
- data/public/railwatch/assets/empty-state-6j_0AaQQ.js +1 -0
- data/public/railwatch/assets/env-layout-DrT8rO6P.js +1 -0
- data/public/railwatch/assets/execution-path-CzgBUi5e.js +1 -0
- data/public/railwatch/assets/filter-bar-9SU5NrzX.js +1 -0
- data/public/railwatch/assets/flamegraph-qcekju8V.js +2 -0
- data/public/railwatch/assets/format-B9SDkrWj.js +1 -0
- data/public/railwatch/assets/frames-Cyu7KMxZ.js +1 -0
- data/public/railwatch/assets/google-sign-in-button-DsTSfmzY.js +1 -0
- data/public/railwatch/assets/index-1ol1-QWI.js +1 -0
- data/public/railwatch/assets/index-5jI4aFzC.js +1 -0
- data/public/railwatch/assets/index-9KTrVnrc.js +1 -0
- data/public/railwatch/assets/index-B0-8lcTp.js +1 -0
- data/public/railwatch/assets/index-B7jjfNfO.js +1 -0
- data/public/railwatch/assets/index-BBchRy0M.js +1 -0
- data/public/railwatch/assets/index-BRiq3SNR.js +1 -0
- data/public/railwatch/assets/index-BgKj9xhr.js +1 -0
- data/public/railwatch/assets/index-BkTZqqOu.js +1 -0
- data/public/railwatch/assets/index-BprKx8QO.js +1 -0
- data/public/railwatch/assets/index-C3jzvPs3.js +1 -0
- data/public/railwatch/assets/index-Cbs6gGyQ.js +1 -0
- data/public/railwatch/assets/index-CdRZ6AWF.js +1 -0
- data/public/railwatch/assets/index-CeYKnapu.js +1 -0
- data/public/railwatch/assets/index-Cmlwy1-V.js +1 -0
- data/public/railwatch/assets/index-Cwx6058d.js +1 -0
- data/public/railwatch/assets/index-D4CSdbHv.js +1 -0
- data/public/railwatch/assets/index-DEFMSkdG.js +1 -0
- data/public/railwatch/assets/index-DFiHEBSh.js +1 -0
- data/public/railwatch/assets/index-DL4vWdWJ.js +1 -0
- data/public/railwatch/assets/index-DU9F5b5d.js +1 -0
- data/public/railwatch/assets/index-DaXgPcGL.js +1 -0
- data/public/railwatch/assets/index-DbtaU-EE.js +1 -0
- data/public/railwatch/assets/index-DeOe83F4.js +1 -0
- data/public/railwatch/assets/index-DiucHN4B.js +1 -0
- data/public/railwatch/assets/index-DlnR_l9o.js +1 -0
- data/public/railwatch/assets/index-DlumCsWY.js +2 -0
- data/public/railwatch/assets/index-DmRd7aIG.js +1 -0
- data/public/railwatch/assets/index-DxSh2UpM.js +1 -0
- data/public/railwatch/assets/index-MIMGuFNt.js +1 -0
- data/public/railwatch/assets/index-OqI59zPb.js +1 -0
- data/public/railwatch/assets/index-P4rC7IlX.js +1 -0
- data/public/railwatch/assets/index-gpPOcFWq.js +1 -0
- data/public/railwatch/assets/index-oVkururr.js +1 -0
- data/public/railwatch/assets/index-p9puqVge.js +1 -0
- data/public/railwatch/assets/inertia-TViv6kNv.js +97 -0
- data/public/railwatch/assets/input-error-LxImUkxv.js +1 -0
- data/public/railwatch/assets/json-viewer-Ar4cjPDW.js +1 -0
- data/public/railwatch/assets/klass-CJ-J4INB.js +1 -0
- data/public/railwatch/assets/label-COUKWqE_.js +1 -0
- data/public/railwatch/assets/layout-DNSLAkw_.js +1 -0
- data/public/railwatch/assets/live-dot-ChfUtY3p.js +41 -0
- data/public/railwatch/assets/nav-CNnDqPlm.js +1 -0
- data/public/railwatch/assets/new-84S8ZJq9.js +1 -0
- data/public/railwatch/assets/new-Be55nmt9.js +1 -0
- data/public/railwatch/assets/new-Bi_xQiIb.js +1 -0
- data/public/railwatch/assets/new-BvCT8TMg.js +1 -0
- data/public/railwatch/assets/new-D05SajFR.js +1 -0
- data/public/railwatch/assets/new-D4uewYC8.js +1 -0
- data/public/railwatch/assets/onboarding-CYZi5Cqc.js +1 -0
- data/public/railwatch/assets/origin-identity-6q1-CBts.js +1 -0
- data/public/railwatch/assets/percentile-picker-gFZCXtdb.js +1 -0
- data/public/railwatch/assets/relative-time-IOOgl5n2.js +1 -0
- data/public/railwatch/assets/release-health-DC8oc7uw.js +1 -0
- data/public/railwatch/assets/route-Dv6LAWvT.js +1 -0
- data/public/railwatch/assets/segmented-h1VdDTqE.js +1 -0
- data/public/railwatch/assets/select-_AJsUa7X.js +1 -0
- data/public/railwatch/assets/separator-BwwTYtCF.js +1 -0
- data/public/railwatch/assets/series-chart-DaFPefku.js +1 -0
- data/public/railwatch/assets/show-B7NCgkEo.js +1 -0
- data/public/railwatch/assets/show-BKqyKjBK.js +1 -0
- data/public/railwatch/assets/show-BM6X2Mpo.js +1 -0
- data/public/railwatch/assets/show-BNw4tN5q.js +1 -0
- data/public/railwatch/assets/show-BO3bnG5h.js +1 -0
- data/public/railwatch/assets/show-BhrAVAEA.js +1 -0
- data/public/railwatch/assets/show-C4Ltf5i9.js +2 -0
- data/public/railwatch/assets/show-C8sHalnw.js +1 -0
- data/public/railwatch/assets/show-CeTL4B37.js +2 -0
- data/public/railwatch/assets/show-CpfgV1jP.js +1 -0
- data/public/railwatch/assets/show-DACku6AD.js +3 -0
- data/public/railwatch/assets/show-DIOSGcXV.js +6 -0
- data/public/railwatch/assets/show-DQp_1n-B.js +1 -0
- data/public/railwatch/assets/show-DVNz46RI.js +1 -0
- data/public/railwatch/assets/show-DYteoYWW.js +1 -0
- data/public/railwatch/assets/show-DgSIoRvA.js +1 -0
- data/public/railwatch/assets/show-JxFtB4eK.js +2 -0
- data/public/railwatch/assets/sort-header-DpFzXblu.js +1 -0
- data/public/railwatch/assets/source-link-B2183i2-.js +1 -0
- data/public/railwatch/assets/sparkline-cell-C3-5vFkP.js +1 -0
- data/public/railwatch/assets/stat-s4RpOS9w.js +1 -0
- data/public/railwatch/assets/status-badge-8jVV-LA4.js +1 -0
- data/public/railwatch/assets/tenant-path-G-6u9A-o.js +1 -0
- data/public/railwatch/assets/text-link-DfsiaCcP.js +1 -0
- data/public/railwatch/assets/textarea-Dye72uP7.js +1 -0
- data/public/railwatch/assets/timeline-CD7WHnbo.js +1 -0
- data/public/railwatch/assets/transition-B_AW8rMK.js +5 -0
- data/public/railwatch/assets/use-clipboard-ColgLyQ2.js +1 -0
- data/public/railwatch/icon.png +0 -0
- data/public/railwatch/icon.svg +5 -0
- data/public/railwatch/manifest.json +2171 -0
- data/public/railwatch/rails-vite.json +1 -0
- metadata +313 -4
|
@@ -67,7 +67,13 @@ module Railwatch
|
|
|
67
67
|
streaming: p[:streaming] == true,
|
|
68
68
|
message_count: p[:message_count],
|
|
69
69
|
tool_count: Array(p[:tools]).size,
|
|
70
|
+
tools: tool_names(p),
|
|
70
71
|
cost_nanos: cost_nanos(p),
|
|
72
|
+
cost_reported: cost_reported(p),
|
|
73
|
+
finish_reason: finish_reason(p),
|
|
74
|
+
provider_request_id: provider_request_id(p),
|
|
75
|
+
params: params(operation, p),
|
|
76
|
+
**attachments(p),
|
|
71
77
|
**tokens(p),
|
|
72
78
|
**workflow(p),
|
|
73
79
|
**outcome(p),
|
|
@@ -103,6 +109,8 @@ module Railwatch
|
|
|
103
109
|
provider: p[:provider].to_s,
|
|
104
110
|
model: p[:model].to_s,
|
|
105
111
|
tool_name: tool_name[0, 255],
|
|
112
|
+
tool_call_id: p[:tool_call_id]&.to_s&.slice(0, 128),
|
|
113
|
+
params: ({ result_class: p[:result_class].to_s[0, 128] } if p[:result_class]),
|
|
106
114
|
duration: micros(event),
|
|
107
115
|
**workflow(p),
|
|
108
116
|
**outcome(p),
|
|
@@ -110,6 +118,145 @@ module Railwatch
|
|
|
110
118
|
completion: content(p[:result_content]))
|
|
111
119
|
end
|
|
112
120
|
|
|
121
|
+
# Why the model stopped. :max_tokens means the answer was cut off --
|
|
122
|
+
# a truncated extraction reads exactly like a complete one without
|
|
123
|
+
# this, which is the failure most worth being able to see.
|
|
124
|
+
def finish_reason(payload)
|
|
125
|
+
response = payload[:response]
|
|
126
|
+
return nil unless response.respond_to?(:finish_reason)
|
|
127
|
+
|
|
128
|
+
response.finish_reason&.to_s&.slice(0, 32)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Message#raw is the Faraday response (protocol.rb hands it in), so the
|
|
132
|
+
# provider's own request id is in its headers. It is what a provider
|
|
133
|
+
# support ticket asks for, and the only key that joins our record to
|
|
134
|
+
# theirs.
|
|
135
|
+
REQUEST_ID_HEADERS = %w[request-id x-request-id x-amzn-requestid].freeze
|
|
136
|
+
|
|
137
|
+
def provider_request_id(payload)
|
|
138
|
+
raw = payload[:response]
|
|
139
|
+
raw = raw.raw if raw.respond_to?(:raw)
|
|
140
|
+
headers = raw.respond_to?(:headers) ? raw.headers : nil
|
|
141
|
+
return nil unless headers.respond_to?(:[])
|
|
142
|
+
|
|
143
|
+
REQUEST_ID_HEADERS.each do |name|
|
|
144
|
+
value = headers[name]
|
|
145
|
+
return value.to_s[0, 128] if value.present?
|
|
146
|
+
end
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Which tools the model could reach on this call. tool_count alone says
|
|
151
|
+
# how many; retracing needs which.
|
|
152
|
+
def tool_names(payload)
|
|
153
|
+
names = Array(payload[:tools]).map(&:to_s)
|
|
154
|
+
names.empty? ? nil : names.first(50).join(",")[0, 1024]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Whether the provider priced the call itself, or we estimated it from
|
|
158
|
+
# the registry. The difference matters when a total is queried against
|
|
159
|
+
# an invoice.
|
|
160
|
+
def cost_reported(payload)
|
|
161
|
+
tokens = payload[:tokens]
|
|
162
|
+
return nil unless tokens.respond_to?(:reported_cost)
|
|
163
|
+
# No cost means no provenance to report. false would claim the
|
|
164
|
+
# registry priced it, which is the same false certainty cost_nanos
|
|
165
|
+
# avoids by being nil rather than zero.
|
|
166
|
+
return nil if cost_nanos(payload).nil?
|
|
167
|
+
|
|
168
|
+
!tokens.reported_cost.nil?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# What the call carried besides text. Images and PDFs are most of the
|
|
172
|
+
# input tokens on a document-reading call, and without this an
|
|
173
|
+
# expensive scan is indistinguishable from an expensive prompt.
|
|
174
|
+
# Only the last user turn is measured: earlier turns were counted by
|
|
175
|
+
# the calls that sent them, and walking the whole history would both
|
|
176
|
+
# double-count and cost O(messages) on every call.
|
|
177
|
+
def attachments(payload)
|
|
178
|
+
message = last_user_message(payload)
|
|
179
|
+
list = message.respond_to?(:attachments) ? Array(message.attachments) : []
|
|
180
|
+
return {} if list.empty?
|
|
181
|
+
|
|
182
|
+
types = list.filter_map { |a| a.type.to_s if a.respond_to?(:type) }.tally
|
|
183
|
+
.sort_by { |_, n| -n }.map { |type, n| n > 1 ? "#{type}x#{n}" : type }.join(",")
|
|
184
|
+
{ attachments: list.size, attachment_types: types[0, 128],
|
|
185
|
+
attachment_names: content(list.filter_map { |a| a.filename if a.respond_to?(:filename) }.join(", ")) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The knobs that change what a call costs and what it returns, so a
|
|
189
|
+
# surprising result can be reproduced with the settings that produced
|
|
190
|
+
# it. Provider options go through the app's own parameter filter: they
|
|
191
|
+
# are request configuration, but an app can put anything in them.
|
|
192
|
+
COMMON_PARAMS = %i[temperature max_output_tokens tool_choice tool_call_limit
|
|
193
|
+
thinking caching citations dimensions task_type size count
|
|
194
|
+
voice format language pages document_count top_n].freeze
|
|
195
|
+
|
|
196
|
+
def params(operation, payload)
|
|
197
|
+
out = {}
|
|
198
|
+
COMMON_PARAMS.each do |key|
|
|
199
|
+
value = payload[key]
|
|
200
|
+
next if value.nil?
|
|
201
|
+
# false is kept, not dropped: `caching` defaults to nil, so
|
|
202
|
+
# caching: false is a deliberate choice, and reproducing a call
|
|
203
|
+
# needs the settings it ran with. RubyLLM does not distinguish a
|
|
204
|
+
# boolean that was set from one that defaulted, so record both
|
|
205
|
+
# rather than guess which mattered.
|
|
206
|
+
out[key] = value.is_a?(Numeric) || [ true, false ].include?(value) ? value : value.to_s[0, 128]
|
|
207
|
+
end
|
|
208
|
+
out[:schema] = true if payload[:schema]
|
|
209
|
+
out[:server_tools] = Array(payload[:server_tools]).map(&:to_s).first(20) if payload[:server_tools].present?
|
|
210
|
+
if (usage = payload[:tokens]).respond_to?(:server_tool_use) && usage.server_tool_use.present?
|
|
211
|
+
out[:server_tool_use] = usage.server_tool_use
|
|
212
|
+
end
|
|
213
|
+
if (options = payload[:provider_options]).is_a?(Hash) && !options.empty?
|
|
214
|
+
out[:provider_options] = provider_options(options)
|
|
215
|
+
end
|
|
216
|
+
out[:operation] = operation unless out.empty?
|
|
217
|
+
out.empty? ? nil : out
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Two filters, because one is not enough here. The app's parameter
|
|
221
|
+
# filter defaults to password-shaped names only, and provider_options
|
|
222
|
+
# is the one place in this payload where a per-request credential
|
|
223
|
+
# plausibly lives -- an api_key passed per call sails straight through
|
|
224
|
+
# a password filter. The redactor's credential-name matcher (the same
|
|
225
|
+
# one that catches X-Api-Key on a header) closes that.
|
|
226
|
+
def provider_options(options)
|
|
227
|
+
redact_credentials(Railwatch.redactor.params(options.transform_keys(&:to_s)))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Recursive, because provider options nest: extra_headers carrying an
|
|
231
|
+
# authorization value is a hash inside the hash, and a top-level scan
|
|
232
|
+
# walks straight past it.
|
|
233
|
+
def redact_credentials(value, depth = 0)
|
|
234
|
+
return value if depth > 4
|
|
235
|
+
|
|
236
|
+
case value
|
|
237
|
+
when Hash
|
|
238
|
+
value.each_with_object({}) do |(key, item), out|
|
|
239
|
+
# The matcher is written for header names, which are hyphenated;
|
|
240
|
+
# provider options are Ruby-ish and use underscores, so api_key
|
|
241
|
+
# would sail past a pattern expecting api-key.
|
|
242
|
+
out[key] = if Railwatch.redactor.redact_header?(key.to_s.tr("_", "-"))
|
|
243
|
+
Redactor::FILTERED
|
|
244
|
+
else
|
|
245
|
+
redact_credentials(item, depth + 1)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
when Array then value.map { |item| redact_credentials(item, depth + 1) }
|
|
249
|
+
else value
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def last_user_message(payload)
|
|
254
|
+
messages = payload[:input_messages]
|
|
255
|
+
return nil unless messages.respond_to?(:reverse_each)
|
|
256
|
+
|
|
257
|
+
messages.reverse_each.find { |m| m.respond_to?(:role) && m.role.to_s == "user" }
|
|
258
|
+
end
|
|
259
|
+
|
|
113
260
|
# 2.0 sends a RubyLLM::Tokens; 1.16 sends bare counts on the event.
|
|
114
261
|
def tokens(payload)
|
|
115
262
|
counts = payload[:tokens]
|
|
@@ -161,11 +308,9 @@ module Railwatch
|
|
|
161
308
|
# The last thing the app asked, which is the half of a conversation
|
|
162
309
|
# worth seeing next to a cost. Earlier turns are the app's own records.
|
|
163
310
|
def prompt_text(payload)
|
|
164
|
-
|
|
165
|
-
return payload[:input] || payload[:prompt] || payload[:query] unless messages.respond_to?(:reverse_each)
|
|
311
|
+
return payload[:input] || payload[:prompt] || payload[:query] unless payload[:input_messages].respond_to?(:reverse_each)
|
|
166
312
|
|
|
167
|
-
|
|
168
|
-
message_text(last)
|
|
313
|
+
message_text(last_user_message(payload))
|
|
169
314
|
end
|
|
170
315
|
|
|
171
316
|
def message_text(message)
|
|
@@ -89,7 +89,8 @@ module Railwatch
|
|
|
89
89
|
# $PROGRAM_NAME after boot and would otherwise turn the supervisor,
|
|
90
90
|
# dispatcher, and scheduler into "web" on every health sample.
|
|
91
91
|
def role
|
|
92
|
-
if defined?(::
|
|
92
|
+
if defined?(Railwatch::Writer) && Railwatch::Writer.running? then "writer"
|
|
93
|
+
elsif defined?(::SolidQueue) && ($PROGRAM_NAME.include?("jobs") || $PROGRAM_NAME.start_with?("solid-queue-") || ARGV.first.to_s.start_with?("solid_queue:")) then "worker"
|
|
93
94
|
elsif defined?(::Rails::Console) then "console"
|
|
94
95
|
elsif $PROGRAM_NAME.end_with?("rake") then "command"
|
|
95
96
|
elsif defined?(::Puma) then "web"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railwatch
|
|
4
|
+
module Transport
|
|
5
|
+
# Writes each batch straight into the engine's telemetry database instead
|
|
6
|
+
# of POSTing it. Same Reporter, same buffer, same backpressure; the only
|
|
7
|
+
# difference from Transport::Http is that deliver ends in Ingest::Batch
|
|
8
|
+
# rather than Net::HTTP. Runs on the reporter thread, never on a request.
|
|
9
|
+
class Local
|
|
10
|
+
Result = Struct.new(:ok, :status, :accepted, :rejected, :rejections, :error, :retryable_error, keyword_init: true) do
|
|
11
|
+
def retryable? = retryable_error ? true : false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(config)
|
|
15
|
+
@config = config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def deliver(records, dropped: 0, dropped_bytes: 0, backpressure_factor: 1.0, batch_id: nil)
|
|
19
|
+
wire = stringify(records)
|
|
20
|
+
# The reporter thread is not a request or a job: nothing has set up
|
|
21
|
+
# an ExecutionContext for it. Query log tags, Rails.error.report and
|
|
22
|
+
# connection checkin all assume one, so run the write inside the
|
|
23
|
+
# executor the same way a job would.
|
|
24
|
+
#
|
|
25
|
+
# The rescue is INSIDE the wrap. The executor reports any exception
|
|
26
|
+
# that escapes its block to Rails.error before re-raising it, and
|
|
27
|
+
# Railwatch subscribes to Rails.error: a write that failed (the
|
|
28
|
+
# telemetry database not migrated yet, a locked file) would otherwise
|
|
29
|
+
# be captured as one of the application's own exceptions and opened
|
|
30
|
+
# as an issue about Railwatch, by Railwatch.
|
|
31
|
+
Rails.application.executor.wrap do
|
|
32
|
+
write(wire, records.size, dropped: dropped, backpressure_factor: backpressure_factor, batch_id: batch_id)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
Railwatch.debug { "local ingest failed: #{e.class}: #{e.message}" }
|
|
35
|
+
# With a batch id the reporter can safely retry: the ledger says
|
|
36
|
+
# whether the write committed, and the unique execution_id index
|
|
37
|
+
# makes a replay of a half-visible batch a no-op. Without one (an
|
|
38
|
+
# older caller) the batch is consumed, since a retry could
|
|
39
|
+
# double-insert. A malformed record never gets here: the mapper
|
|
40
|
+
# counts it as rejected and the rest of the batch is written.
|
|
41
|
+
Result.new(ok: false, error: "#{e.class}: #{e.message}", retryable_error: !batch_id.nil?)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ping = true
|
|
46
|
+
def unauthorized? = false
|
|
47
|
+
def reset_after_fork! = self
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def write(wire, count, dropped:, backpressure_factor:, batch_id:)
|
|
52
|
+
environment = Environment.current
|
|
53
|
+
# The same contract the HTTP path gets from X-Railwatch-Batch-Id: a
|
|
54
|
+
# batch the reporter retries after a failure is written once. The
|
|
55
|
+
# ledger row is created inside the batch transaction, so its presence
|
|
56
|
+
# is the commit.
|
|
57
|
+
if (ledger = environment.with_telemetry { Ingest::Batch.committed(batch_id) })
|
|
58
|
+
return Result.new(ok: true, status: 200, accepted: ledger.accepted, rejected: ledger.rejected, rejections: [])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
result = Ingest::Batch.new(environment, wire, dropped_by_client: dropped, backpressure_factor: backpressure_factor,
|
|
62
|
+
gem_version: Railwatch::VERSION, embedded: true, batch_id: batch_id).write!
|
|
63
|
+
Result.new(ok: true, status: 200, accepted: result.accepted, rejected: result.rejected,
|
|
64
|
+
rejections: result.rejections.first(10))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The gem builds symbol-keyed records, nested hashes included (stages,
|
|
68
|
+
# counters, headers); the mapper reads string keys throughout and
|
|
69
|
+
# rejects a symbol-keyed structure. A JSON round trip is the deep
|
|
70
|
+
# stringify. Measured against the alternatives on a 500-record batch:
|
|
71
|
+
# 1.9 ms and 11k allocations here versus 4.8 ms and 25k for
|
|
72
|
+
# deep_transform_keys, which also leaves Symbol values as Symbols.
|
|
73
|
+
def stringify(records)
|
|
74
|
+
JSON.parse(JSON.generate(records))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Railwatch
|
|
8
|
+
module Transport
|
|
9
|
+
# Hands each batch to the embedded writer process (Railwatch::Writer) over
|
|
10
|
+
# a Unix socket instead of writing it into SQLite on this thread. The
|
|
11
|
+
# workers' Ruby threads then never map a record, hold SQLite's write lock,
|
|
12
|
+
# or run a t-digest; all of that happens in one process whose GVL no
|
|
13
|
+
# request shares. Same Reporter, same buffer, same batch id, same ledger:
|
|
14
|
+
# a batch the writer already committed replays as a no-op there.
|
|
15
|
+
#
|
|
16
|
+
# One request per connection, length-prefixed, gzip JSON both ways:
|
|
17
|
+
# > [u32 length][gzip(JSON {batch_id, records, dropped, dropped_bytes, backpressure_factor})]
|
|
18
|
+
# < [u32 length][JSON {ok, status, accepted, rejected, rejections, error, retryable_error}]
|
|
19
|
+
#
|
|
20
|
+
# Whether a writer is EXPECTED decides what a missing one means. Under
|
|
21
|
+
# the Puma plugin (which sets Writer.expected! in the master before it
|
|
22
|
+
# forks the workers) a socket that is absent or refusing is a writer
|
|
23
|
+
# that is starting or restarting: the batch is retained and retried on
|
|
24
|
+
# the reporter's backoff ladder, which is bounded -- after
|
|
25
|
+
# Reporter::MAX_RETRY_ATTEMPTS the batch is counted as dropped and
|
|
26
|
+
# reported through on_unrecoverable, rather than held for ever. Anywhere else
|
|
27
|
+
# (a plain `rails server`, a runner, a Solid Queue worker, the suite) no
|
|
28
|
+
# writer will ever appear, so the first miss switches this transport to
|
|
29
|
+
# an in-process Transport::Local for the rest of the process's life and
|
|
30
|
+
# embedded mode works exactly as it did before the writer existed. A
|
|
31
|
+
# refusing socket in that case is a stale inode from a dead writer and
|
|
32
|
+
# falls back the same way rather than dropping batches forever.
|
|
33
|
+
class Socket
|
|
34
|
+
Result = Local::Result
|
|
35
|
+
MAX_REPLY_BYTES = 1 << 20
|
|
36
|
+
# How long an in-process fallback lasts before the socket is tried
|
|
37
|
+
# again. The fallback is provisional on purpose: a process that missed
|
|
38
|
+
# the writer once (a bare `puma` whose plugin could not mark one
|
|
39
|
+
# expected, a worker forked before the writer bound) would otherwise
|
|
40
|
+
# write its own batches for the rest of its life.
|
|
41
|
+
FALLBACK_RECHECK = 30
|
|
42
|
+
# How long a process that EXPECTS a writer keeps retaining batches for
|
|
43
|
+
# one that never answers before it writes them itself. A writer that is
|
|
44
|
+
# restarting is back in seconds; one that cannot bind at all (an
|
|
45
|
+
# unwritable socket directory, a fork that keeps failing) would
|
|
46
|
+
# otherwise retain until the reporter's retry cap and then drop the
|
|
47
|
+
# batch. Writing it here instead keeps the records.
|
|
48
|
+
WRITER_GRACE = 60
|
|
49
|
+
|
|
50
|
+
def initialize(config, path: nil, expected: nil)
|
|
51
|
+
@config = config
|
|
52
|
+
@path = path || config.writer_socket_path
|
|
53
|
+
@expected = expected
|
|
54
|
+
# A path the kernel cannot bind (over 108 bytes on Linux) can never
|
|
55
|
+
# have a writer behind it; do not spend a batch finding out, and do
|
|
56
|
+
# not keep re-checking it either.
|
|
57
|
+
@unusable_path = !Writer.usable_path?(@path)
|
|
58
|
+
@fallback = @unusable_path ? Local.new(config) : nil
|
|
59
|
+
@fallback_at = Clock.monotonic if @fallback
|
|
60
|
+
Railwatch.debug { "writer socket path #{@path.inspect} is too long; writing batches in-process" } if @fallback
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :path
|
|
64
|
+
|
|
65
|
+
def fallback? = !@fallback.nil?
|
|
66
|
+
|
|
67
|
+
# Read when a batch misses, not when the transport is built: under
|
|
68
|
+
# `rails server` the app boots (and the reporter picks this transport)
|
|
69
|
+
# before Puma evaluates config/puma.rb, so the plugin's Writer.expected!
|
|
70
|
+
# lands after construction, and the workers inherit a copy of this
|
|
71
|
+
# object at fork. A flag captured here at construction was always false
|
|
72
|
+
# in every worker, and the first flush before the writer was listening
|
|
73
|
+
# fell back to in-process writes for the life of the worker.
|
|
74
|
+
def expected? = @expected.nil? ? Writer.expected? : @expected
|
|
75
|
+
|
|
76
|
+
def deliver(records, dropped: 0, dropped_bytes: 0, backpressure_factor: 1.0, batch_id: nil)
|
|
77
|
+
return @fallback.deliver(records, dropped: dropped, dropped_bytes: dropped_bytes,
|
|
78
|
+
backpressure_factor: backpressure_factor, batch_id: batch_id) if falling_back?
|
|
79
|
+
|
|
80
|
+
payload = Zlib.gzip(JSON.generate(batch_id: batch_id, records: records, dropped: dropped,
|
|
81
|
+
dropped_bytes: dropped_bytes, backpressure_factor: backpressure_factor))
|
|
82
|
+
reply = exchange(payload)
|
|
83
|
+
Result.new(**reply.slice("ok", "status", "accepted", "rejected", "rejections", "error", "retryable_error").transform_keys(&:to_sym))
|
|
84
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTSOCK => e
|
|
85
|
+
@missing_since ||= Clock.monotonic
|
|
86
|
+
if expected? && Clock.monotonic - @missing_since < WRITER_GRACE
|
|
87
|
+
Railwatch.debug { "writer not answering at #{@path} (#{e.class}); retaining the batch" }
|
|
88
|
+
return Result.new(ok: false, error: "writer unavailable: #{e.message}", retryable_error: true)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Railwatch.debug do
|
|
92
|
+
if expected?
|
|
93
|
+
"no writer at #{@path} after #{WRITER_GRACE}s (#{e.class}); writing batches in-process, re-checking every #{FALLBACK_RECHECK}s"
|
|
94
|
+
else
|
|
95
|
+
"no writer at #{@path} (#{e.class}) and none expected; writing batches in-process, re-checking every #{FALLBACK_RECHECK}s"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
@fallback = Local.new(@config)
|
|
99
|
+
@fallback_at = Clock.monotonic
|
|
100
|
+
deliver(records, dropped: dropped, dropped_bytes: dropped_bytes, backpressure_factor: backpressure_factor, batch_id: batch_id)
|
|
101
|
+
rescue SystemCallError, IOError, Zlib::Error, JSON::ParserError, Timeout::Error => e
|
|
102
|
+
Railwatch.debug { "writer delivery failed: #{e.class}: #{e.message}" }
|
|
103
|
+
Result.new(ok: false, error: "#{e.class}: #{e.message}", retryable_error: true)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Whether this batch goes in-process. A fallback for an unbindable path
|
|
107
|
+
# is permanent; any other one is re-examined every FALLBACK_RECHECK
|
|
108
|
+
# seconds, so a writer that turns up later takes the work back.
|
|
109
|
+
def falling_back?
|
|
110
|
+
return false if @fallback.nil?
|
|
111
|
+
return true if @unusable_path || Clock.monotonic - @fallback_at < FALLBACK_RECHECK
|
|
112
|
+
|
|
113
|
+
if Writer.listening?(@path)
|
|
114
|
+
Railwatch.debug { "writer is answering at #{@path} again; handing batches back to it" }
|
|
115
|
+
@fallback = nil
|
|
116
|
+
@missing_since = nil
|
|
117
|
+
false
|
|
118
|
+
else
|
|
119
|
+
@fallback_at = Clock.monotonic
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ping
|
|
125
|
+
return true if @fallback
|
|
126
|
+
|
|
127
|
+
UNIXSocket.new(@path).close
|
|
128
|
+
true
|
|
129
|
+
rescue SystemCallError, ArgumentError
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def unauthorized? = false
|
|
134
|
+
def reset_after_fork! = self
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# Every read and write is under one deadline of config.timeout, the
|
|
139
|
+
# same budget the HTTP transport gives a POST; a writer that accepts but
|
|
140
|
+
# stops reading cannot hold the reporter thread past it.
|
|
141
|
+
def exchange(payload)
|
|
142
|
+
UNIXSocket.open(@path) do |sock|
|
|
143
|
+
deadline = Clock.monotonic + @config.timeout
|
|
144
|
+
write_exactly(sock, [ payload.bytesize ].pack("N") + payload, deadline)
|
|
145
|
+
header = read_exactly(sock, 4, deadline)
|
|
146
|
+
length = header.unpack1("N")
|
|
147
|
+
raise IOError, "writer reply too large (#{length} bytes)" if length > MAX_REPLY_BYTES
|
|
148
|
+
|
|
149
|
+
JSON.parse(read_exactly(sock, length, deadline))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def write_exactly(sock, data, deadline)
|
|
154
|
+
offset = 0
|
|
155
|
+
while offset < data.bytesize
|
|
156
|
+
remaining = deadline - Clock.monotonic
|
|
157
|
+
raise Timeout::Error, "writer did not accept the batch within #{@config.timeout}s" if remaining <= 0
|
|
158
|
+
raise IOError, "writer closed the connection" unless sock.wait_writable(remaining)
|
|
159
|
+
|
|
160
|
+
written = sock.write_nonblock(data.byteslice(offset..), exception: false)
|
|
161
|
+
offset += written if written.is_a?(Integer)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def read_exactly(sock, count, deadline)
|
|
166
|
+
buffer = +""
|
|
167
|
+
while buffer.bytesize < count
|
|
168
|
+
remaining = deadline - Clock.monotonic
|
|
169
|
+
raise Timeout::Error, "writer did not answer within #{@config.timeout}s" if remaining <= 0
|
|
170
|
+
raise IOError, "writer closed the connection" unless sock.wait_readable(remaining)
|
|
171
|
+
|
|
172
|
+
chunk = sock.read_nonblock(count - buffer.bytesize, exception: false)
|
|
173
|
+
case chunk
|
|
174
|
+
when :wait_readable then next
|
|
175
|
+
when nil then raise IOError, "writer closed the connection"
|
|
176
|
+
else buffer << chunk
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
buffer
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/railwatch/version.rb
CHANGED