forge_ops_tracker 0.12.0 → 0.13.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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +46 -0
- data/lib/forge_ops_tracker/change.rb +47 -0
- data/lib/forge_ops_tracker/change_snapshot.rb +133 -0
- data/lib/forge_ops_tracker/client.rb +11 -0
- data/lib/forge_ops_tracker/configuration.rb +29 -0
- data/lib/forge_ops_tracker/delivery_queue.rb +5 -2
- data/lib/forge_ops_tracker/railtie.rb +8 -0
- data/lib/forge_ops_tracker/version.rb +1 -1
- data/lib/forge_ops_tracker.rb +31 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccbeed25a3172994d20455c19fa79a8336f3c73324ed667fd2f31d2cc05a19f5
|
|
4
|
+
data.tar.gz: 3cb7e1562daf2ccb8f09226bc915b7f00f6e8486ebbf361f8adf0a16c9851afe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6287470f8bc768e95d1d1afe55f2c7a0e640a4d66ded8a0e3c518e0248031a7f5f76621adf0436d2de3bc7da029733b9debab07e31d2f1b37c7867cfd3aee7e5
|
|
7
|
+
data.tar.gz: b7c996fbf89c2c0a90f6c39c848542cde734242a3fa4efaf450a36b1e7362815322417ceafc89d05a71a453a9965d7b4ab0c4deb2eedad0fdafd6b21808ad790
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 (2026-09-25)
|
|
4
|
+
|
|
5
|
+
- New `ForgeOpsTracker.record_change(kind:, title:, details: {}, environment: nil, service: nil,
|
|
6
|
+
actor: nil, url: nil, id: nil, occurred_at: nil)` records something that changed in your system
|
|
7
|
+
(a feature flag, a config value, a hand-run migration) so ForgeOps can show it next to the errors
|
|
8
|
+
that followed. `kind` is one of `feature_flag`, `config`, `migration`, `dependency`,
|
|
9
|
+
`infrastructure`, or `other`; anything else is sent as `other`. Sent from a background thread,
|
|
10
|
+
never raises, and a no-op wherever reporting is off.
|
|
11
|
+
- Changes between deploys are now detected automatically. Once per process, after Rails finishes
|
|
12
|
+
booting (or on `ForgeOpsTracker.configure` outside Rails), a background thread sends a snapshot of
|
|
13
|
+
the Ruby version, the gem versions in `Gemfile.lock`, and the current Active Record migration
|
|
14
|
+
version, and ForgeOps records whatever changed since the previous boot. New
|
|
15
|
+
`config.detect_changes` (default true) turns this off.
|
|
16
|
+
- New `config.track_env_var_names` (default false) adds environment variable names, never values,
|
|
17
|
+
to that snapshot. Host-specific names (`HOSTNAME`, `PATH`, `PORT`, `LC_*`, Kubernetes service
|
|
18
|
+
variables, and others) and the gem's own `FORGE_OPS_*` settings are always left out.
|
|
19
|
+
|
|
3
20
|
## 0.12.0
|
|
4
21
|
|
|
5
22
|
- Every error event reported during a request now says where it happened: `transaction_name` (the
|
data/README.md
CHANGED
|
@@ -411,6 +411,52 @@ Requires a ForgeOps plan that includes distributed tracing; on a plan that doesn
|
|
|
411
411
|
simply rejected server-side and dropped, exactly like any other delivery failure. The `trace_id`
|
|
412
412
|
on error events is sent either way.
|
|
413
413
|
|
|
414
|
+
## What changed
|
|
415
|
+
|
|
416
|
+
ForgeOps can show what changed in your system next to the errors and slowdowns that followed it.
|
|
417
|
+
Two ways in:
|
|
418
|
+
|
|
419
|
+
**Record a change yourself** when something changes that no deploy captures, like a feature flag
|
|
420
|
+
flipped, a config value edited, or a migration run by hand:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
ForgeOpsTracker.record_change(
|
|
424
|
+
kind: "feature_flag", # feature_flag, config, migration, dependency, infrastructure, or other
|
|
425
|
+
title: "Enabled new_checkout for 10% of users",
|
|
426
|
+
details: { flag: "new_checkout", rollout_percent: 10 },
|
|
427
|
+
actor: current_user.email,
|
|
428
|
+
url: "https://flags.example.com/new_checkout"
|
|
429
|
+
)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`kind` and `title` are required; `details`, `environment` (defaults to `config.environment`),
|
|
433
|
+
`service`, `actor`, `url`, `id` (an idempotency key, so sending the same change twice records it
|
|
434
|
+
once), and `occurred_at` (defaults to now) are optional. An unknown `kind` is sent as `other`. It's
|
|
435
|
+
queued and sent from a background thread, so it never slows down the caller, never raises, and is
|
|
436
|
+
a no-op wherever reporting is off.
|
|
437
|
+
|
|
438
|
+
**Changes between deploys are detected for you.** Once per process, right after Rails finishes
|
|
439
|
+
booting, a background thread sends ForgeOps a snapshot of what the app is running: the Ruby
|
|
440
|
+
version, every gem version in your `Gemfile.lock`, and the current Active Record migration version
|
|
441
|
+
(left out if the database can't be reached). ForgeOps compares it with the previous boot's and
|
|
442
|
+
records whatever changed, such as a gem upgrade or a new migration. Outside Rails the snapshot goes
|
|
443
|
+
out when you call `ForgeOpsTracker.configure`, without the migration version.
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
ForgeOpsTracker.configure do |config|
|
|
447
|
+
config.detect_changes = true # default; false sends no startup snapshot
|
|
448
|
+
config.track_env_var_names = false # default; true also sends environment variable names
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
With `track_env_var_names` on, the snapshot lists the names of your environment variables (never
|
|
453
|
+
their values), so an added or removed variable shows up as a change. Names that differ from host to
|
|
454
|
+
host, like `HOSTNAME`, `PATH`, `PORT`, `LC_*`, and Kubernetes service variables, are left out, as
|
|
455
|
+
are the gem's own `FORGE_OPS_*` settings.
|
|
456
|
+
|
|
457
|
+
Requires a ForgeOps plan that includes change tracking; on a plan that doesn't, both are rejected
|
|
458
|
+
server-side and dropped, exactly like any other delivery failure.
|
|
459
|
+
|
|
414
460
|
## Custom metrics
|
|
415
461
|
|
|
416
462
|
Unlike session/performance tracking above, there's no automatic instrumentation here at all: a
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module ForgeOpsTracker
|
|
4
|
+
# Builds the body for one ForgeOpsTracker.record_change call (POST /api/v1/changes): something
|
|
5
|
+
# that changed in a running system (a feature flag flipped, a config value edited, a migration
|
|
6
|
+
# run by hand), so ForgeOps can line it up against the errors and slowdowns around it.
|
|
7
|
+
module Change
|
|
8
|
+
# The server rejects any other kind outright (422), so an unknown one is sent as "other"
|
|
9
|
+
# rather than dropped: the change still gets recorded, just less specifically categorized.
|
|
10
|
+
KINDS = %w[feature_flag config migration dependency infrastructure other].freeze
|
|
11
|
+
MAX_TITLE_LENGTH = 200
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Returns nil when there's nothing sendable (a blank title): the server requires one, so
|
|
16
|
+
# posting it anyway would only ever come back 422.
|
|
17
|
+
def build(configuration, kind:, title:, details: {}, environment: nil, service: nil, actor: nil, url: nil, id: nil, occurred_at: nil)
|
|
18
|
+
title = title.to_s.strip
|
|
19
|
+
return nil if title.empty?
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
kind: normalize_kind(kind),
|
|
23
|
+
title: title[0, MAX_TITLE_LENGTH],
|
|
24
|
+
details: details.is_a?(Hash) ? details : {},
|
|
25
|
+
environment: (environment || configuration.environment)&.to_s,
|
|
26
|
+
service: service&.to_s,
|
|
27
|
+
actor: actor&.to_s,
|
|
28
|
+
url: url&.to_s,
|
|
29
|
+
id: id&.to_s,
|
|
30
|
+
occurred_at: iso8601(occurred_at || Time.now)
|
|
31
|
+
}.compact
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def normalize_kind(kind)
|
|
35
|
+
kind = kind.to_s
|
|
36
|
+
KINDS.include?(kind) ? kind : "other"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def iso8601(value)
|
|
40
|
+
case value
|
|
41
|
+
when Time then value.utc.iso8601(3)
|
|
42
|
+
when String then value
|
|
43
|
+
else value.respond_to?(:to_time) ? value.to_time.utc.iso8601(3) : Time.now.utc.iso8601(3)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module ForgeOpsTracker
|
|
4
|
+
# Tells ForgeOps what this process sees at startup (the Ruby version, the locked gem versions, the
|
|
5
|
+
# current Active Record migration version, and optionally the names of its environment variables),
|
|
6
|
+
# once per process, from a background thread. The server diffs it against the last snapshot for
|
|
7
|
+
# the same project and environment and records whatever changed as a change, so this gem stays
|
|
8
|
+
# stateless: it never remembers what it sent last time.
|
|
9
|
+
#
|
|
10
|
+
# Any key that can't be determined reliably is left out rather than guessed at, since the server
|
|
11
|
+
# reads a missing key as "unknown", never as "everything was removed".
|
|
12
|
+
class ChangeSnapshot
|
|
13
|
+
MAX_DEPENDENCIES = 3000
|
|
14
|
+
MAX_NAME_LENGTH = 200
|
|
15
|
+
MAX_VERSION_LENGTH = 100
|
|
16
|
+
MAX_ENV_VAR_NAMES = 3000
|
|
17
|
+
|
|
18
|
+
# Host-specific variables that differ from one box to the next (or one boot to the next) without
|
|
19
|
+
# anything about the deploy having changed, so a fleet doesn't look like it's changing on every
|
|
20
|
+
# restart. The gem's own FORGE_OPS_* settings are dropped too.
|
|
21
|
+
ENV_VAR_DENYLIST = %w[
|
|
22
|
+
HOSTNAME HOST HOME PATH PWD OLDPWD SHLVL _ TERM USER LOGNAME SHELL LANG TMPDIR TZ PORT DYNO
|
|
23
|
+
INVOCATION_ID JOURNAL_STREAM
|
|
24
|
+
].freeze
|
|
25
|
+
ENV_VAR_DENYLIST_PATTERNS = [
|
|
26
|
+
/\ALC_/, /\ASYSTEMD_/, /\AMEMORY_PRESSURE_/, /\AKUBERNETES_/, /\AFORGE_OPS_/,
|
|
27
|
+
/_SERVICE_HOST\z/, /_SERVICE_PORT/, /_PORT_.*_TCP/
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
MUTEX = Mutex.new
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Starts the one snapshot this process sends, on its own thread so it never delays boot. A
|
|
34
|
+
# second call is a no-op, so the Railtie and a plain ForgeOpsTracker.configure can't both
|
|
35
|
+
# send one. Does nothing (and doesn't use up the once) when the gem isn't enabled or
|
|
36
|
+
# detect_changes is off. Never raises.
|
|
37
|
+
def start(configuration)
|
|
38
|
+
return false unless configuration.enabled? && configuration.detect_changes
|
|
39
|
+
|
|
40
|
+
MUTEX.synchronize do
|
|
41
|
+
return false if @started
|
|
42
|
+
|
|
43
|
+
@started = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
thread = Thread.new { new(configuration).deliver }
|
|
47
|
+
thread.report_on_exception = false
|
|
48
|
+
true
|
|
49
|
+
rescue StandardError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Names only, never values, sorted so the same set always serializes the same way.
|
|
54
|
+
def env_var_names(env = ENV)
|
|
55
|
+
env.keys.reject { |name| denied_env_var?(name) }.sort.first(MAX_ENV_VAR_NAMES)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def denied_env_var?(name)
|
|
59
|
+
ENV_VAR_DENYLIST.include?(name) || ENV_VAR_DENYLIST_PATTERNS.any? { |pattern| pattern.match?(name) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(configuration, client: Client.new(configuration))
|
|
64
|
+
@configuration = configuration
|
|
65
|
+
@client = client
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def deliver
|
|
69
|
+
client.deliver_change_snapshot(payload)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
configuration.logger&.debug { "[ForgeOpsTracker] change snapshot failed: #{e.class}: #{e.message}" }
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def payload
|
|
76
|
+
{
|
|
77
|
+
environment: configuration.environment.to_s,
|
|
78
|
+
state: {
|
|
79
|
+
schema_version: schema_version,
|
|
80
|
+
runtime: runtime,
|
|
81
|
+
dependencies: dependencies,
|
|
82
|
+
env_var_names: configuration.track_env_var_names ? self.class.env_var_names : nil
|
|
83
|
+
}.compact
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def runtime
|
|
88
|
+
"#{RUBY_ENGINE} #{RUBY_ENGINE_VERSION}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The app's Gemfile.lock (through Bundler, so it's the exact resolved versions), falling back to
|
|
92
|
+
# whatever gems are actually activated when there's no bundle at all. nil, and so left out,
|
|
93
|
+
# when neither is available.
|
|
94
|
+
def dependencies
|
|
95
|
+
specs = bundler_specs || Gem.loaded_specs.values
|
|
96
|
+
return nil if specs.empty?
|
|
97
|
+
|
|
98
|
+
specs.each_with_object({}) do |spec, deps|
|
|
99
|
+
deps[spec.name.to_s[0, MAX_NAME_LENGTH]] = spec.version.to_s[0, MAX_VERSION_LENGTH]
|
|
100
|
+
end.sort.first(MAX_DEPENDENCIES).to_h
|
|
101
|
+
rescue StandardError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The newest migration Active Record has run against this app's database, as a string. Left
|
|
106
|
+
# out when Active Record isn't loaded, the database can't be reached, or nothing has been
|
|
107
|
+
# migrated yet: a snapshot must never fail (or fail boot) because the database is down.
|
|
108
|
+
def schema_version
|
|
109
|
+
return nil unless defined?(::ActiveRecord::Base)
|
|
110
|
+
|
|
111
|
+
version = ::ActiveRecord::Base.connection_pool.with_connection do |connection|
|
|
112
|
+
pool = ::ActiveRecord::Base.connection_pool
|
|
113
|
+
context = pool.respond_to?(:migration_context) ? pool.migration_context : connection.migration_context
|
|
114
|
+
context.current_version
|
|
115
|
+
end
|
|
116
|
+
version.to_i.positive? ? version.to_s : nil
|
|
117
|
+
rescue StandardError, ScriptError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
attr_reader :configuration, :client
|
|
123
|
+
|
|
124
|
+
def bundler_specs
|
|
125
|
+
return nil unless defined?(::Bundler)
|
|
126
|
+
|
|
127
|
+
specs = ::Bundler.locked_gems&.specs
|
|
128
|
+
specs.nil? || specs.empty? ? nil : specs
|
|
129
|
+
rescue StandardError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -60,6 +60,17 @@ module ForgeOpsTracker
|
|
|
60
60
|
post(configuration.spans_uri, { trace_id: trace_id, spans: spans })
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Same delivery contract again; see Configuration#changes_uri and ForgeOpsTracker.record_change.
|
|
64
|
+
# One change per call. A plan without change tracking answers 403, which is just a false here.
|
|
65
|
+
def deliver_change(payload)
|
|
66
|
+
post(configuration.changes_uri, payload)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Same again; see Configuration#change_snapshots_uri and ChangeSnapshot.
|
|
70
|
+
def deliver_change_snapshot(payload)
|
|
71
|
+
post(configuration.change_snapshots_uri, payload)
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
private
|
|
64
75
|
attr_reader :configuration
|
|
65
76
|
|
|
@@ -18,6 +18,7 @@ module ForgeOpsTracker
|
|
|
18
18
|
attr_accessor :gauge_poll_interval
|
|
19
19
|
attr_accessor :track_tracing, :trace_capture_threshold_ms
|
|
20
20
|
attr_accessor :propagate_traces, :trace_propagation_targets
|
|
21
|
+
attr_accessor :detect_changes, :track_env_var_names
|
|
21
22
|
|
|
22
23
|
def initialize
|
|
23
24
|
@dsn = ENV["FORGE_OPS_DSN"]
|
|
@@ -138,6 +139,15 @@ module ForgeOpsTracker
|
|
|
138
139
|
# Regexp is matched against the host. Worth narrowing for an app that calls third-party APIs
|
|
139
140
|
# which reject unknown headers, or that shouldn't learn this app's trace ids at all.
|
|
140
141
|
@trace_propagation_targets = nil
|
|
142
|
+
# Sends one ChangeSnapshot per process at startup (Ruby version, locked gem versions, the
|
|
143
|
+
# current migration version), which ForgeOps diffs against the previous boot's to record what
|
|
144
|
+
# changed between deploys. On by default, like every other automatic behavior here.
|
|
145
|
+
@detect_changes = true
|
|
146
|
+
# Whether that snapshot also lists the names of this process's environment variables, so an
|
|
147
|
+
# added or removed variable shows up as a change. Off by default: names only, never values,
|
|
148
|
+
# but even names can describe more about an app than some teams want to share. See
|
|
149
|
+
# ChangeSnapshot::ENV_VAR_DENYLIST for the host-specific names that are always left out.
|
|
150
|
+
@track_env_var_names = false
|
|
141
151
|
end
|
|
142
152
|
|
|
143
153
|
# Whether an outbound request to host should carry a traceparent header; see
|
|
@@ -231,11 +241,30 @@ module ForgeOpsTracker
|
|
|
231
241
|
uri
|
|
232
242
|
end
|
|
233
243
|
|
|
244
|
+
def changes_uri
|
|
245
|
+
sibling_uri("/changes")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def change_snapshots_uri
|
|
249
|
+
sibling_uri("/change_snapshots")
|
|
250
|
+
end
|
|
251
|
+
|
|
234
252
|
def enabled?
|
|
235
253
|
!blank?(dsn) && !blank?(api_key) && enabled_environments.map(&:to_s).include?(environment.to_s)
|
|
236
254
|
end
|
|
237
255
|
|
|
238
256
|
private
|
|
257
|
+
# Same substitution every *_uri method above spells out by hand: a sibling path under the
|
|
258
|
+
# DSN's own /api/v1/events.
|
|
259
|
+
def sibling_uri(path)
|
|
260
|
+
uri = ingestion_uri
|
|
261
|
+
return nil unless uri
|
|
262
|
+
|
|
263
|
+
uri = uri.dup
|
|
264
|
+
uri.path = uri.path.sub(%r{/events\z}, path)
|
|
265
|
+
uri
|
|
266
|
+
end
|
|
267
|
+
|
|
239
268
|
def parsed_dsn
|
|
240
269
|
return nil if blank?(dsn)
|
|
241
270
|
|
|
@@ -8,9 +8,12 @@ module ForgeOpsTracker
|
|
|
8
8
|
# in each forked Puma/Passenger worker process rather than being carried
|
|
9
9
|
# across a fork, which would leave it dead in the child.
|
|
10
10
|
class DeliveryQueue
|
|
11
|
-
|
|
11
|
+
# deliver_with names the Client method each payload goes through: :deliver (error events) by
|
|
12
|
+
# default, :deliver_change for ForgeOpsTracker.record_change's own queue.
|
|
13
|
+
def initialize(configuration, client: Client.new(configuration), deliver_with: :deliver)
|
|
12
14
|
@configuration = configuration
|
|
13
15
|
@client = client
|
|
16
|
+
@deliver_with = deliver_with
|
|
14
17
|
@queue = SizedQueue.new(configuration.queue_size)
|
|
15
18
|
@mutex = Mutex.new
|
|
16
19
|
@thread = nil
|
|
@@ -44,7 +47,7 @@ module ForgeOpsTracker
|
|
|
44
47
|
|
|
45
48
|
def run
|
|
46
49
|
loop do
|
|
47
|
-
client.
|
|
50
|
+
client.public_send(@deliver_with, @queue.pop)
|
|
48
51
|
rescue StandardError => e
|
|
49
52
|
log { "delivery thread error: #{e.class}: #{e.message}" }
|
|
50
53
|
end
|
|
@@ -305,6 +305,14 @@ module ForgeOpsTracker
|
|
|
305
305
|
# sidekiq, ActiveJob's redis queue adapter, ...), which this gem has no control over; deferring
|
|
306
306
|
# the defined? check to after every railtie (including this app's own dependencies) has
|
|
307
307
|
# finished initializing avoids a load-order-dependent false negative.
|
|
308
|
+
# After initialization, not in a plain initializer, so the app's own
|
|
309
|
+
# config/initializers/forge_ops_tracker.rb has already run (detect_changes/track_env_var_names
|
|
310
|
+
# and the DSN are final) and Active Record is configured enough to report the migration
|
|
311
|
+
# version. ChangeSnapshot does the actual work on its own thread, so boot never waits on it.
|
|
312
|
+
config.after_initialize do
|
|
313
|
+
ForgeOpsTracker::ChangeSnapshot.start(ForgeOpsTracker.configuration)
|
|
314
|
+
end
|
|
315
|
+
|
|
308
316
|
config.after_initialize do
|
|
309
317
|
next unless defined?(::RedisClient)
|
|
310
318
|
|
data/lib/forge_ops_tracker.rb
CHANGED
|
@@ -6,6 +6,8 @@ require "forge_ops_tracker/sql_statement"
|
|
|
6
6
|
require "forge_ops_tracker/event_builder"
|
|
7
7
|
require "forge_ops_tracker/client"
|
|
8
8
|
require "forge_ops_tracker/delivery_queue"
|
|
9
|
+
require "forge_ops_tracker/change"
|
|
10
|
+
require "forge_ops_tracker/change_snapshot"
|
|
9
11
|
require "forge_ops_tracker/error_subscriber"
|
|
10
12
|
require "forge_ops_tracker/session_flusher"
|
|
11
13
|
require "forge_ops_tracker/histogram_bucketer"
|
|
@@ -32,8 +34,33 @@ module ForgeOpsTracker
|
|
|
32
34
|
@configuration ||= Configuration.new
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# Outside Rails this is also where the startup ChangeSnapshot goes out, since there's no
|
|
38
|
+
# Railtie to send it once the app has booted; under Rails the Railtie sends it instead, after
|
|
39
|
+
# initialization, when Active Record can report the migration version.
|
|
35
40
|
def configure
|
|
36
41
|
yield configuration
|
|
42
|
+
ChangeSnapshot.start(configuration) unless defined?(::Rails::Railtie)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Records one thing that changed in a running system, e.g.
|
|
46
|
+
# `ForgeOpsTracker.record_change(kind: "feature_flag", title: "Enabled new_checkout for 10%")`,
|
|
47
|
+
# so ForgeOps can show it next to the errors and slowdowns that followed. kind is one of
|
|
48
|
+
# Change::KINDS (anything else is sent as "other"); environment defaults to the configured one.
|
|
49
|
+
# Delivered from a background thread like error events, so it never blocks the caller, and a
|
|
50
|
+
# no-op when the gem isn't enabled. Never raises: returns false when nothing was queued.
|
|
51
|
+
def record_change(kind:, title:, details: {}, environment: nil, service: nil, actor: nil, url: nil, id: nil, occurred_at: nil)
|
|
52
|
+
return false unless configuration.enabled?
|
|
53
|
+
|
|
54
|
+
payload = Change.build(
|
|
55
|
+
configuration, kind: kind, title: title, details: details, environment: environment,
|
|
56
|
+
service: service, actor: actor, url: url, id: id, occurred_at: occurred_at
|
|
57
|
+
)
|
|
58
|
+
return false unless payload
|
|
59
|
+
|
|
60
|
+
change_queue.push(payload)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
configuration.logger&.debug { "[ForgeOpsTracker] record_change failed: #{e.class}: #{e.message}" }
|
|
63
|
+
false
|
|
37
64
|
end
|
|
38
65
|
|
|
39
66
|
# Records a named business metric (a signup, a payment, anything a customer wants to name),
|
|
@@ -120,6 +147,10 @@ module ForgeOpsTracker
|
|
|
120
147
|
end
|
|
121
148
|
|
|
122
149
|
private
|
|
150
|
+
def change_queue
|
|
151
|
+
@change_queue ||= DeliveryQueue.new(configuration, deliver_with: :deliver_change)
|
|
152
|
+
end
|
|
153
|
+
|
|
123
154
|
def metric_buffer
|
|
124
155
|
@metric_buffer ||= MetricBuffer.new(configuration)
|
|
125
156
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forge_ops_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ForgeOps
|
|
@@ -90,6 +90,8 @@ files:
|
|
|
90
90
|
- README.md
|
|
91
91
|
- lib/forge_ops_tracker.rb
|
|
92
92
|
- lib/forge_ops_tracker/breadcrumb_buffer.rb
|
|
93
|
+
- lib/forge_ops_tracker/change.rb
|
|
94
|
+
- lib/forge_ops_tracker/change_snapshot.rb
|
|
93
95
|
- lib/forge_ops_tracker/client.rb
|
|
94
96
|
- lib/forge_ops_tracker/configuration.rb
|
|
95
97
|
- lib/forge_ops_tracker/delivery_queue.rb
|