railwatch 0.1.0
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 +7 -0
- data/AGENTS.md +122 -0
- data/CHANGELOG.md +462 -0
- data/MIT-LICENSE +20 -0
- data/README.md +226 -0
- data/app/controllers/railwatch/beacon_controller.rb +254 -0
- data/config/routes.rb +5 -0
- data/docs/ai-and-mcp.md +227 -0
- data/docs/configuration.md +931 -0
- data/docs/faq.md +230 -0
- data/docs/getting-started.md +279 -0
- data/docs/records.md +834 -0
- data/docs/replacing-nightwatch.md +216 -0
- data/docs/replacing-sentry.md +573 -0
- data/docs/security.md +94 -0
- data/docs/self-hosting.md +60 -0
- data/docs/source-maps.md +60 -0
- data/docs/testing.md +175 -0
- data/docs/troubleshooting.md +319 -0
- data/lib/generators/railwatch/install/install_generator.rb +280 -0
- data/lib/generators/railwatch/install/templates/initializer.rb +54 -0
- data/lib/generators/railwatch/install/templates/post-deploy +98 -0
- data/lib/generators/railwatch/install/templates/railwatch.ts +658 -0
- data/lib/railwatch/attachments.rb +83 -0
- data/lib/railwatch/backtrace.rb +158 -0
- data/lib/railwatch/buffer.rb +122 -0
- data/lib/railwatch/clock.rb +25 -0
- data/lib/railwatch/configuration.rb +334 -0
- data/lib/railwatch/console.rb +48 -0
- data/lib/railwatch/context.rb +125 -0
- data/lib/railwatch/controller_helpers.rb +21 -0
- data/lib/railwatch/current.rb +32 -0
- data/lib/railwatch/engine.rb +144 -0
- data/lib/railwatch/execution.rb +367 -0
- data/lib/railwatch/faraday.rb +73 -0
- data/lib/railwatch/health.rb +188 -0
- data/lib/railwatch/job_tracing.rb +49 -0
- data/lib/railwatch/middleware/request.rb +289 -0
- data/lib/railwatch/minitest.rb +43 -0
- data/lib/railwatch/patches/inertia.rb +34 -0
- data/lib/railwatch/patches/net_http.rb +102 -0
- data/lib/railwatch/patches/rake_task.rb +88 -0
- data/lib/railwatch/patches/runner_command.rb +120 -0
- data/lib/railwatch/patches.rb +43 -0
- data/lib/railwatch/profiler.rb +270 -0
- data/lib/railwatch/record.rb +119 -0
- data/lib/railwatch/redactor.rb +67 -0
- data/lib/railwatch/release_detector.rb +97 -0
- data/lib/railwatch/reporter.rb +539 -0
- data/lib/railwatch/rspec.rb +139 -0
- data/lib/railwatch/sampler.rb +17 -0
- data/lib/railwatch/secret_safety.rb +62 -0
- data/lib/railwatch/sessions.rb +162 -0
- data/lib/railwatch/source_maps.rb +59 -0
- data/lib/railwatch/spec_helper.rb +147 -0
- data/lib/railwatch/sql_normalizer.rb +398 -0
- data/lib/railwatch/subscribers/base.rb +54 -0
- data/lib/railwatch/subscribers/broadcasts.rb +107 -0
- data/lib/railwatch/subscribers/cache.rb +107 -0
- data/lib/railwatch/subscribers/deprecations.rb +26 -0
- data/lib/railwatch/subscribers/exceptions.rb +304 -0
- data/lib/railwatch/subscribers/jobs.rb +282 -0
- data/lib/railwatch/subscribers/logs.rb +137 -0
- data/lib/railwatch/subscribers/mail.rb +42 -0
- data/lib/railwatch/subscribers/notifications.rb +36 -0
- data/lib/railwatch/subscribers/process_info.rb +98 -0
- data/lib/railwatch/subscribers/queries.rb +183 -0
- data/lib/railwatch/subscribers/requests.rb +94 -0
- data/lib/railwatch/subscribers/storage.rb +35 -0
- data/lib/railwatch/subscribers/users.rb +159 -0
- data/lib/railwatch/subscribers/views.rb +54 -0
- data/lib/railwatch/subscribers.rb +34 -0
- data/lib/railwatch/transport/http.rb +208 -0
- data/lib/railwatch/version.rb +5 -0
- data/lib/railwatch.rb +550 -0
- data/lib/tasks/railwatch_tasks.rake +289 -0
- data/llms.txt +38 -0
- metadata +157 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "railwatch/secret_safety"
|
|
5
|
+
|
|
6
|
+
module Railwatch
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Creates config/initializers/railwatch.rb, a Kamal post-deploy hook, the browser client, and wires the test helpers."
|
|
12
|
+
|
|
13
|
+
class_option :token, type: :string,
|
|
14
|
+
desc: "Deprecated: token in process arguments. Prefer --prompt-token, --token-stdin, or RAILWATCH_TOKEN."
|
|
15
|
+
class_option :prompt_token, type: :boolean, default: false,
|
|
16
|
+
desc: "Prompt for the ingest token without echoing it."
|
|
17
|
+
class_option :token_stdin, type: :boolean, default: false,
|
|
18
|
+
desc: "Read the ingest token from one line on standard input."
|
|
19
|
+
class_option :url, type: :string,
|
|
20
|
+
desc: "Ingest URL, for a self-hosted Railwatch Cloud. Defaults to https://railwatch.rebulk.com."
|
|
21
|
+
class_option :kamal_secrets, type: :boolean, default: false,
|
|
22
|
+
desc: "Wire RAILWATCH_TOKEN through .kamal/secrets and config/deploy.yml's `env: secret:` list."
|
|
23
|
+
class_option :doctor, type: :boolean, default: true,
|
|
24
|
+
desc: "Run railwatch:doctor when the install finishes."
|
|
25
|
+
|
|
26
|
+
# Whichever of these exists is where `startRailwatch()` is added.
|
|
27
|
+
INERTIA_ENTRYPOINTS = %w[
|
|
28
|
+
app/frontend/entrypoints/inertia.tsx
|
|
29
|
+
app/frontend/entrypoints/inertia.ts
|
|
30
|
+
app/frontend/entrypoints/inertia.jsx
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
IMPORT_LINE = 'import { startRailwatch } from "@/lib/railwatch"'
|
|
34
|
+
START_CALL = "startRailwatch()"
|
|
35
|
+
|
|
36
|
+
# The name is written in three places -- .env, .kamal/secrets, and
|
|
37
|
+
# config/deploy.yml -- and each of them has to be idempotent, so it is
|
|
38
|
+
# spelled once here.
|
|
39
|
+
TOKEN_VAR = "RAILWATCH_TOKEN"
|
|
40
|
+
URL_VAR = "RAILWATCH_INGEST_URL"
|
|
41
|
+
|
|
42
|
+
def create_initializer
|
|
43
|
+
template "initializer.rb", "config/initializers/railwatch.rb"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_kamal_hook
|
|
47
|
+
return unless File.exist?("config/deploy.yml")
|
|
48
|
+
template "post-deploy", ".kamal/hooks/post-deploy"
|
|
49
|
+
chmod ".kamal/hooks/post-deploy", 0o755
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_browser_client
|
|
53
|
+
return unless File.directory?("app/frontend")
|
|
54
|
+
template "railwatch.ts", "app/frontend/lib/railwatch.ts"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Adds the import and the call to the Inertia entrypoint, so page-visit
|
|
58
|
+
# timing and Core Web Vitals report with no further editing.
|
|
59
|
+
def start_browser_client
|
|
60
|
+
return unless File.directory?("app/frontend")
|
|
61
|
+
|
|
62
|
+
path = INERTIA_ENTRYPOINTS.find { |candidate| File.exist?(candidate) }
|
|
63
|
+
return say(browser_client_instructions(INERTIA_ENTRYPOINTS.first), :yellow) unless path
|
|
64
|
+
|
|
65
|
+
contents = File.read(path)
|
|
66
|
+
return say_status(:identical, path, :blue) if contents.include?(START_CALL)
|
|
67
|
+
|
|
68
|
+
imports = contents.lines.select { |line| line.match?(/\A\s*import\s/) }
|
|
69
|
+
return say(browser_client_instructions(path), :yellow) if imports.empty?
|
|
70
|
+
|
|
71
|
+
# Anchored at \A so the injection lands after the *last* import line
|
|
72
|
+
# and can only ever match once, however many imports repeat.
|
|
73
|
+
inject_into_file path, "#{IMPORT_LINE}\n\n#{START_CALL}\n",
|
|
74
|
+
after: /\A[\s\S]*#{Regexp.escape(imports.last)}/
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# `require "railwatch/rspec"` / `"railwatch/minitest"` brings in the block
|
|
78
|
+
# matchers (have_railwatch_queries, have_railwatch_n_plus_one, ...) that turn
|
|
79
|
+
# a spec suite into a performance gate. See docs/testing.md.
|
|
80
|
+
def wire_test_helper
|
|
81
|
+
if File.exist?("spec/rails_helper.rb")
|
|
82
|
+
inject_require "spec/rails_helper.rb", 'require "railwatch/rspec"', %r{^require ["']rspec/rails["'].*\n}
|
|
83
|
+
elsif File.exist?("test/test_helper.rb")
|
|
84
|
+
inject_require "test/test_helper.rb", 'require "railwatch/minitest"', %r{^require ["']rails/test_help["'].*\n}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def mount_beacon
|
|
89
|
+
route 'mount Railwatch::Engine, at: "/railwatch"'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A token lands in .env only when Git confirms the file is ignored.
|
|
93
|
+
# URLs are not secret and can still be written to a tracked dotenv file.
|
|
94
|
+
def write_env
|
|
95
|
+
token = resolved_token
|
|
96
|
+
if options[:token]
|
|
97
|
+
say("--token exposes #{Railwatch::SecretSafety.token_preview(options[:token])} in process arguments; " \
|
|
98
|
+
"use --prompt-token or --token-stdin next time.", :yellow)
|
|
99
|
+
end
|
|
100
|
+
vars = { TOKEN_VAR => token, URL_VAR => options[:url] }.compact
|
|
101
|
+
return if vars.empty?
|
|
102
|
+
return say(env_instructions(vars), :yellow) unless dotenv_app?
|
|
103
|
+
|
|
104
|
+
if token && !safe_dotenv_for_token?
|
|
105
|
+
say("Refusing to write #{Railwatch::SecretSafety.token_preview(token)} to .env because Git does not confirm that .env is ignored. Use Rails credentials, a secret manager, or add .env to .gitignore first.", :red)
|
|
106
|
+
vars.delete(TOKEN_VAR)
|
|
107
|
+
return if vars.empty?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
existing = File.exist?(".env") ? File.read(".env") : ""
|
|
111
|
+
missing = vars.reject { |name, _| existing.match?(/^#{name}=/) }
|
|
112
|
+
return say_status(:identical, ".env", :blue) if missing.empty?
|
|
113
|
+
|
|
114
|
+
body = missing.map { |name, value| "#{name}=#{value}\n" }.join
|
|
115
|
+
if File.exist?(".env")
|
|
116
|
+
append_to_file ".env", (existing.end_with?("\n") || existing.empty? ? body : "\n#{body}")
|
|
117
|
+
else
|
|
118
|
+
create_file ".env", body
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Kamal reads .kamal/secrets with shell expansion and passes only the
|
|
123
|
+
# names listed under `env: secret:` into the container, so the token
|
|
124
|
+
# needs both halves to reach the app.
|
|
125
|
+
def configure_kamal_secrets
|
|
126
|
+
return unless options[:kamal_secrets]
|
|
127
|
+
return say("--kamal-secrets: no .kamal/secrets found; run `bin/kamal init` first.", :yellow) unless File.exist?(".kamal/secrets")
|
|
128
|
+
|
|
129
|
+
secrets = File.read(".kamal/secrets")
|
|
130
|
+
if secrets.match?(/^#{TOKEN_VAR}=/)
|
|
131
|
+
say_status(:identical, ".kamal/secrets", :blue)
|
|
132
|
+
else
|
|
133
|
+
append_to_file ".kamal/secrets", "#{secrets.end_with?("\n") ? "" : "\n"}#{TOKEN_VAR}=$#{TOKEN_VAR}\n"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
return say("--kamal-secrets: no config/deploy.yml found; add #{TOKEN_VAR} under `env: secret:` yourself.", :yellow) unless File.exist?("config/deploy.yml")
|
|
137
|
+
|
|
138
|
+
deploy = File.read("config/deploy.yml")
|
|
139
|
+
updated = self.class.deploy_yml_with_secret(deploy)
|
|
140
|
+
return say_status(:identical, "config/deploy.yml", :blue) if updated == deploy
|
|
141
|
+
|
|
142
|
+
create_file "config/deploy.yml", updated, force: true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def show_next_steps
|
|
146
|
+
say <<~STEPS, :green
|
|
147
|
+
|
|
148
|
+
Next steps
|
|
149
|
+
1. Set #{TOKEN_VAR}. With Kamal, add it to .kamal/secrets:
|
|
150
|
+
#{TOKEN_VAR}=$#{TOKEN_VAR}
|
|
151
|
+
and list it under `env: secret:` in config/deploy.yml (or re-run
|
|
152
|
+
this generator with --kamal-secrets). Otherwise put it in
|
|
153
|
+
credentials and read it in the initializer:
|
|
154
|
+
c.token = Rails.application.credentials.dig(:railwatch, :token)
|
|
155
|
+
Self-hosting? Set #{URL_VAR} too.
|
|
156
|
+
No token yet? bin/rails railwatch:token
|
|
157
|
+
2. Verify the install: bin/rails railwatch:doctor
|
|
158
|
+
3. Connect your AI assistant: bin/rails railwatch:mcp
|
|
159
|
+
4. Gate performance in CI: see docs/testing.md.
|
|
160
|
+
STEPS
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Runs the same checklist the developer would run next, in this
|
|
164
|
+
# process. Railwatch's configuration was read at boot, so a token this
|
|
165
|
+
# run just wrote to .env is not visible until the app restarts -- the
|
|
166
|
+
# note below says so rather than letting a ✗ look like a broken install.
|
|
167
|
+
def run_doctor
|
|
168
|
+
return unless options[:doctor]
|
|
169
|
+
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
170
|
+
|
|
171
|
+
say "\nbin/rails railwatch:doctor", :green
|
|
172
|
+
require "rake"
|
|
173
|
+
Rails.application.load_tasks unless Rake::Task.task_defined?("railwatch:doctor")
|
|
174
|
+
Rake::Task["railwatch:doctor"].reenable
|
|
175
|
+
Rake::Task["railwatch:doctor"].invoke
|
|
176
|
+
rescue SystemExit
|
|
177
|
+
say "\nFix the ✗ lines above and re-run `bin/rails railwatch:doctor`. " \
|
|
178
|
+
"Values added to .env or credentials just now are only picked up after a restart.", :yellow
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
say "\nCould not run railwatch:doctor here (#{e.class}: #{e.message}). Run `bin/rails railwatch:doctor` yourself.", :yellow
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Adds RAILWATCH_TOKEN to config/deploy.yml's `env: secret:` list as a
|
|
184
|
+
# targeted text insertion. A YAML round-trip would be shorter and would
|
|
185
|
+
# throw away every comment in the file, which is most of what a Kamal
|
|
186
|
+
# deploy.yml is. Returns the contents unchanged when it is already
|
|
187
|
+
# listed.
|
|
188
|
+
def self.deploy_yml_with_secret(contents, name = TOKEN_VAR)
|
|
189
|
+
lines = contents.lines
|
|
190
|
+
env_start = lines.index { |line| line.match?(/\Aenv:\s*(#.*)?$/) }
|
|
191
|
+
return "#{contents.sub(/\n*\z/, "\n")}\nenv:\n secret:\n - #{name}\n" unless env_start
|
|
192
|
+
|
|
193
|
+
env_end = block_end(lines, env_start)
|
|
194
|
+
return contents if lines[env_start...env_end].any? { |line| line.match?(/\A\s*-\s*#{name}\s*\z/) }
|
|
195
|
+
|
|
196
|
+
secret_start = (env_start + 1...env_end).find { |i| lines[i].match?(/\A\s+secret:\s*(#.*)?$/) }
|
|
197
|
+
return insert_lines(lines, env_start + 1, " secret:\n - #{name}\n") unless secret_start
|
|
198
|
+
|
|
199
|
+
insert_lines(lines, block_end(lines, secret_start), " - #{name}\n")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Index of the first line after the block opened at `start`: the next
|
|
203
|
+
# line indented no more deeply than it, ignoring blanks and comments,
|
|
204
|
+
# then backed up over any trailing blank lines so an insertion lands
|
|
205
|
+
# inside the block rather than after the gap below it.
|
|
206
|
+
def self.block_end(lines, start)
|
|
207
|
+
indent = lines[start][/\A */].length
|
|
208
|
+
stop = ((start + 1)...lines.length).find { |i|
|
|
209
|
+
line = lines[i]
|
|
210
|
+
next false if line.strip.empty? || line.strip.start_with?("#")
|
|
211
|
+
line[/\A */].length <= indent
|
|
212
|
+
} || lines.length
|
|
213
|
+
stop -= 1 while stop > start + 1 && lines[stop - 1].strip.empty?
|
|
214
|
+
stop
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def self.insert_lines(lines, index, text)
|
|
218
|
+
(lines[0...index] + [ text ] + lines[index..]).join
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private_class_method :block_end, :insert_lines
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def resolved_token
|
|
226
|
+
@resolved_token ||= begin
|
|
227
|
+
value = if options[:prompt_token]
|
|
228
|
+
ask("Railwatch ingest token (input hidden):", echo: false)
|
|
229
|
+
elsif options[:token_stdin]
|
|
230
|
+
$stdin.gets
|
|
231
|
+
elsif options[:token]
|
|
232
|
+
options[:token]
|
|
233
|
+
elsif !ENV[TOKEN_VAR].to_s.empty?
|
|
234
|
+
ENV[TOKEN_VAR]
|
|
235
|
+
end
|
|
236
|
+
value = value.to_s.strip
|
|
237
|
+
value unless value.empty?
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def safe_dotenv_for_token?
|
|
242
|
+
!Railwatch::SecretSafety.git_tracked?(".env") && Railwatch::SecretSafety.git_ignored?(".env")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# dotenv is the only place the generator will write a token: an app
|
|
246
|
+
# that keeps its environment anywhere else gets told what to paste.
|
|
247
|
+
def dotenv_app?
|
|
248
|
+
File.exist?(".env") || (File.exist?("Gemfile") && File.read("Gemfile").match?(/^\s*gem ["']dotenv/))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def env_instructions(vars)
|
|
252
|
+
lines = vars.map do |name, value|
|
|
253
|
+
if name == TOKEN_VAR
|
|
254
|
+
" #{name}=#{Railwatch::SecretSafety.token_preview(value)} (value hidden)"
|
|
255
|
+
else
|
|
256
|
+
" #{name}=#{value}"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
"Set these where this app reads its environment (.kamal/secrets, credentials, or your PaaS config). " \
|
|
260
|
+
"The token value is never printed:\n#{lines.join("\n")}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def browser_client_instructions(path)
|
|
264
|
+
"Add these two lines to #{path} to report Inertia visits and Core Web Vitals:\n" \
|
|
265
|
+
" #{IMPORT_LINE}\n #{START_CALL}"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# inject_into_file on its own would append a second copy on a re-run --
|
|
269
|
+
# Thor only skips when the replacement is already byte-identical in
|
|
270
|
+
# place, and the anchor is not.
|
|
271
|
+
def inject_require(path, line, anchor)
|
|
272
|
+
contents = File.read(path)
|
|
273
|
+
return say_status(:identical, path, :blue) if contents.include?(line)
|
|
274
|
+
return say("Add `#{line}` to #{path} to get the Railwatch test matchers.", :yellow) unless contents.match?(anchor)
|
|
275
|
+
|
|
276
|
+
inject_into_file path, "#{line}\n", after: anchor
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Railwatch: first-class monitoring for Rails. Every option here can also be
|
|
4
|
+
# set by the RAILWATCH_* env var named in the comment.
|
|
5
|
+
Railwatch.configure do |c|
|
|
6
|
+
# c.token = ENV["RAILWATCH_TOKEN"] # RAILWATCH_TOKEN (required)
|
|
7
|
+
# c.ingest_url = "https://railwatch.rebulk.com" # RAILWATCH_INGEST_URL
|
|
8
|
+
# c.deploy = "release-name" # RAILWATCH_DEPLOY; platform/Git auto-detected
|
|
9
|
+
# c.detect_deploy = false # RAILWATCH_DETECT_DEPLOY; default true
|
|
10
|
+
|
|
11
|
+
# Sampling is decided once per execution; a sampled-in request ships its
|
|
12
|
+
# whole tree of queries, cache events, jobs, mail, and logs.
|
|
13
|
+
# c.sample = { requests: 1.0, jobs: 1.0, commands: 1.0, scheduled_tasks: 1.0, channels: 1.0, exceptions: 1.0 }
|
|
14
|
+
|
|
15
|
+
# Drop whole record types: :queries, :cache_events, :mail, :broadcasts,
|
|
16
|
+
# :notifications, :outgoing_requests, :storage_ops, :view_renders, :logs, :transactions
|
|
17
|
+
# c.ignore = []
|
|
18
|
+
|
|
19
|
+
# Default vendor rake tasks (db:migrate, assets:precompile, ...) and default
|
|
20
|
+
# vendor cache-key prefixes (rack::attack, flipper, ...) are excluded unless
|
|
21
|
+
# you opt back in.
|
|
22
|
+
# c.capture_default_vendor_commands = false # RAILWATCH_CAPTURE_DEFAULT_VENDOR_COMMANDS
|
|
23
|
+
# c.capture_default_vendor_cache_keys = false # RAILWATCH_CAPTURE_DEFAULT_VENDOR_CACHE_KEYS
|
|
24
|
+
|
|
25
|
+
# c.log_level = :info
|
|
26
|
+
# c.buffer_bytes = 16 * 1024 * 1024 # reporter queue memory ceiling
|
|
27
|
+
# c.execution_buffer_bytes = 8 * 1024 * 1024
|
|
28
|
+
# c.batch_bytes = 8 * 1024 * 1024 # uncompressed NDJSON per request
|
|
29
|
+
# c.backpressure = true # adapt sampling under buffer/ingest pressure
|
|
30
|
+
# c.backpressure_high_water = 0.8 # fraction of either buffer ceiling
|
|
31
|
+
# c.capture_request_payload = false # only captured for requests that raised, always redacted
|
|
32
|
+
# Retried job errors are usually expected, so they are not captured by
|
|
33
|
+
# default; enabling this can flood the issues list when retries are common.
|
|
34
|
+
# c.capture_job_retry_errors = false # RAILWATCH_CAPTURE_JOB_RETRY_ERRORS
|
|
35
|
+
# c.redact_headers += %w[X-Api-Key]
|
|
36
|
+
# c.redact_params += %w[ssn] # merged with Rails.application.config.filter_parameters
|
|
37
|
+
# c.ignored_request_paths += ["/internal/health"] # /up and /railwatch/beacon are ignored by default
|
|
38
|
+
|
|
39
|
+
# How the current user is described. Default reads Current.user then Warden.
|
|
40
|
+
# c.user { |user| { id: user.id, name: user.name, email: user.email } }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A trailing "*" matches as a prefix; a string starting with "^" (or another
|
|
44
|
+
# regex metacharacter) is compiled as a Regexp; anything else must match the
|
|
45
|
+
# cache key exactly.
|
|
46
|
+
# Railwatch.reject_cache_keys %w[session: rack::attack* ^feature_flag_\d+$]
|
|
47
|
+
# Railwatch.reject_outgoing_requests { |r| r[:host] == "127.0.0.1" }
|
|
48
|
+
# Railwatch.redact_queries { |q| q[:sql] = q[:sql].gsub(/email = '[^']+'/, "email = '?'") }
|
|
49
|
+
# Railwatch.before_ingest { |batch| batch.size < 10_000 } # return false to drop a batch
|
|
50
|
+
|
|
51
|
+
# Called whenever Railwatch rescues one of its own internal errors (a
|
|
52
|
+
# subscriber raising, or delivery failing after its retry), instead of only
|
|
53
|
+
# logging to Railwatch.debug.
|
|
54
|
+
# Railwatch.on_unrecoverable { |error| Rails.error.report(error, handled: true) }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Kamal post-deploy hook: records the deploy in Railwatch so charts and issues
|
|
3
|
+
# get a deploy marker, regressions are attributed to a version, and the
|
|
4
|
+
# platform can show a diff of the commits that shipped.
|
|
5
|
+
#
|
|
6
|
+
# This runs on the deployer machine, which -- unlike the app containers -- has
|
|
7
|
+
# both the git history and the KAMAL_* environment
|
|
8
|
+
# (https://kamal-deploy.org/docs/hooks/overview/), so it POSTs straight to the
|
|
9
|
+
# ingest host instead of shelling into a container. Never fails a deploy.
|
|
10
|
+
set -e
|
|
11
|
+
[ -z "$RAILWATCH_TOKEN" ] && exit 0
|
|
12
|
+
|
|
13
|
+
# Optional: .kamal/hooks/post-deploy --sourcemaps, or set
|
|
14
|
+
# RAILWATCH_SOURCEMAPS=true. Requires the build artifacts on the deployer.
|
|
15
|
+
# Prefer uploading in CI before publishing the image. Deletion is opt-in
|
|
16
|
+
# with RAILWATCH_SOURCEMAPS_DELETE=true and happens only after acknowledgment.
|
|
17
|
+
if [ "${1:-}" = "--sourcemaps" ] || [ "${RAILWATCH_SOURCEMAPS:-}" = "true" ]; then
|
|
18
|
+
RAILWATCH_DEPLOY="$KAMAL_VERSION" bin/rails railwatch:sourcemaps || echo "Railwatch source map upload failed" >&2
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if ! command -v curl >/dev/null 2>&1 || ! command -v ruby >/dev/null 2>&1 || [ -z "$RAILWATCH_INGEST_URL" ]; then
|
|
22
|
+
# Fall back to the rake task inside the deployed container. It records the
|
|
23
|
+
# same deploy minus the commit list, which a container has no git history
|
|
24
|
+
# to build.
|
|
25
|
+
bin/kamal app exec --primary --reuse "bin/rails railwatch:deploy[$KAMAL_VERSION]" || true
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
INGEST="${RAILWATCH_INGEST_URL%/}"
|
|
30
|
+
|
|
31
|
+
case "$INGEST" in
|
|
32
|
+
https://*|http://localhost|http://localhost:*|http://127.0.0.1|http://127.0.0.1:*|http://\[::1\]|http://\[::1\]:*) ;;
|
|
33
|
+
http://*)
|
|
34
|
+
if [ "${RAILWATCH_ALLOW_HTTP:-}" != "true" ]; then
|
|
35
|
+
echo "Railwatch deploy marker skipped: plain HTTP ingest requires RAILWATCH_ALLOW_HTTP=true" >&2
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
|
|
41
|
+
post() {
|
|
42
|
+
curl -sS -m 10 -X POST "$1" \
|
|
43
|
+
-H "Authorization: Bearer $RAILWATCH_TOKEN" \
|
|
44
|
+
-H "Content-Type: application/json" \
|
|
45
|
+
-d "$2" >/dev/null 2>&1 || true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
git_log() {
|
|
49
|
+
if [ -d .git ] && command -v git >/dev/null 2>&1; then
|
|
50
|
+
git log -n 50 --format='%H%x1f%an%x1f%s%x1f%cI' 2>/dev/null || true
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Set RAILWATCH_DEPLOY_URL to link the marker at a CI run or a release page.
|
|
55
|
+
DEPLOY_BODY=$(git_log | ruby -rjson -e '
|
|
56
|
+
commits = $stdin.read.split("\n").reject(&:empty?).map do |line|
|
|
57
|
+
sha, author, message, at = line.split("\x1f")
|
|
58
|
+
{ sha: sha, author: author, message: message, at: at }
|
|
59
|
+
end
|
|
60
|
+
puts JSON.generate(
|
|
61
|
+
deploy: ENV["KAMAL_VERSION"],
|
|
62
|
+
ref: ENV["KAMAL_VERSION"],
|
|
63
|
+
name: ENV["KAMAL_SERVICE_VERSION"],
|
|
64
|
+
url: ENV["RAILWATCH_DEPLOY_URL"],
|
|
65
|
+
server: ENV["KAMAL_HOSTS"].to_s.split(",").first,
|
|
66
|
+
timestamp: ENV["KAMAL_RECORDED_AT"],
|
|
67
|
+
performer: ENV["KAMAL_PERFORMER"],
|
|
68
|
+
destination: ENV["KAMAL_DESTINATION"],
|
|
69
|
+
service: ENV["KAMAL_SERVICE"],
|
|
70
|
+
commits: commits)
|
|
71
|
+
') || DEPLOY_BODY=""
|
|
72
|
+
|
|
73
|
+
if [ -n "$DEPLOY_BODY" ]; then
|
|
74
|
+
post "$INGEST/ingest/deploys" "$DEPLOY_BODY"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Which servers this deploy targeted, so the platform knows who should be
|
|
78
|
+
# reporting. The docs name the role variable KAMAL_ROLE; KAMAL_ROLES is read
|
|
79
|
+
# first because some Kamal versions set the plural.
|
|
80
|
+
KAMAL_BODY=$(ruby -rjson -e '
|
|
81
|
+
list = ->(value) { value.to_s.split(",").map(&:strip).reject(&:empty?) }
|
|
82
|
+
puts JSON.generate(
|
|
83
|
+
version: ENV["KAMAL_VERSION"],
|
|
84
|
+
hosts: list.call(ENV["KAMAL_HOSTS"]),
|
|
85
|
+
roles: list.call(ENV["KAMAL_ROLES"] || ENV["KAMAL_ROLE"]),
|
|
86
|
+
performer: ENV["KAMAL_PERFORMER"],
|
|
87
|
+
destination: ENV["KAMAL_DESTINATION"],
|
|
88
|
+
service: ENV["KAMAL_SERVICE"],
|
|
89
|
+
recorded_at: ENV["KAMAL_RECORDED_AT"],
|
|
90
|
+
command: ENV["KAMAL_COMMAND"],
|
|
91
|
+
subcommand: ENV["KAMAL_SUBCOMMAND"])
|
|
92
|
+
') || KAMAL_BODY=""
|
|
93
|
+
|
|
94
|
+
if [ -n "$KAMAL_BODY" ]; then
|
|
95
|
+
post "$INGEST/ingest/kamal" "$KAMAL_BODY"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
exit 0
|