forge_ops_tracker 0.2.1 → 0.7.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.
@@ -2,6 +2,20 @@ require "rails/railtie"
2
2
 
3
3
  module ForgeOpsTracker
4
4
  class Railtie < ::Rails::Railtie
5
+ # Shared across every performance-timing subscription/integration below (controller, query,
6
+ # job, raw Sidekiq, outbound Net::HTTP): one flusher, one background thread, one set of
7
+ # buckets, since PerformanceFlusher already buckets on [transaction_name, kind] and safely
8
+ # handles concurrent calls from several sources at once. Exposed as a class method, not a
9
+ # local variable inside one initializer, specifically so the Sidekiq/Net::HTTP integrations
10
+ # (each guarded by their own separate initializer, since each only loads if its own optional
11
+ # library is present) can record into the exact same instance rather than each starting a
12
+ # redundant flusher/thread of their own.
13
+ class << self
14
+ def performance_flusher
15
+ @performance_flusher ||= ForgeOpsTracker::PerformanceFlusher.new(ForgeOpsTracker.configuration)
16
+ end
17
+ end
18
+
5
19
  initializer "forge_ops_tracker.subscribe_error_reporter" do |app|
6
20
  configuration = ForgeOpsTracker.configuration
7
21
  configuration.app_root ||= app.root.to_s
@@ -10,5 +24,114 @@ module ForgeOpsTracker
10
24
 
11
25
  Rails.error.subscribe(ForgeOpsTracker::ErrorSubscriber.new(configuration))
12
26
  end
27
+
28
+ initializer "forge_ops_tracker.track_sessions" do |app|
29
+ app.middleware.use ForgeOpsTracker::Middleware::SessionTracking, configuration: ForgeOpsTracker.configuration
30
+ end
31
+
32
+ # Warden::Manager (mounted automatically by Devise, or directly by any other Warden-based
33
+ # auth setup) sets env["warden"] itself; this only ever reads it, so it works regardless of
34
+ # exactly where in the middleware stack this ends up relative to Warden::Manager, as long as
35
+ # that key exists in env by the time this middleware's own #call runs, which is true for any
36
+ # Warden-based auth setup: it mounts its own middleware once, for the whole app, long before
37
+ # any individual request begins.
38
+ initializer "forge_ops_tracker.track_current_user" do |app|
39
+ app.middleware.use ForgeOpsTracker::Middleware::UserContext, configuration: ForgeOpsTracker.configuration
40
+ end
41
+
42
+ # Rails already fires "process_action.action_controller" after every request with the
43
+ # controller/action and how long it took (event.duration, in ms) built in, so unlike session
44
+ # tracking above (which needed a Rack middleware because there's no single existing hook for
45
+ # "a request finished"), this needs no middleware of its own at all, just a subscriber.
46
+ # Guarded the same way the error subscriber and session-tracking middleware both are: on by
47
+ # default, off entirely if track_performance is turned off or the gem itself isn't enabled
48
+ # (no DSN configured, or the current environment isn't in enabled_environments).
49
+ #
50
+ # sql.active_record/perform.active_job are subscribed here too, not in their own separate
51
+ # initializers: all three (plus Sidekiq/Net::HTTP below) are genuinely the same concern
52
+ # (automatic performance timing), sharing the one performance_flusher above; see
53
+ # PerformanceInstrumentation for the actual transaction_name/skip decisions each one makes,
54
+ # kept out of these blocks specifically so that decision logic is unit-testable without a real
55
+ # Rails/ActiveJob boot (this gem's own test suite doesn't require Rails at all; see that
56
+ # file's own comment).
57
+ initializer "forge_ops_tracker.track_performance" do
58
+ configuration = ForgeOpsTracker.configuration
59
+ flusher = Railtie.performance_flusher
60
+
61
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
62
+ next unless configuration.track_performance && configuration.enabled?
63
+
64
+ event = ActiveSupport::Notifications::Event.new(*args)
65
+ transaction_name = PerformanceInstrumentation.controller_transaction_name(event.payload)
66
+ flusher.record(transaction_name: transaction_name, duration_ms: event.duration, kind: "controller")
67
+ end
68
+
69
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
70
+ next unless configuration.track_performance && configuration.enabled?
71
+
72
+ event = ActiveSupport::Notifications::Event.new(*args)
73
+ transaction_name = PerformanceInstrumentation.query_transaction_name(event.payload)
74
+ next if transaction_name.nil? # SCHEMA/cached, see PerformanceInstrumentation's own comment
75
+
76
+ flusher.record(transaction_name: transaction_name, duration_ms: event.duration, kind: "query")
77
+ end
78
+
79
+ ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
80
+ next unless configuration.track_performance && configuration.enabled?
81
+
82
+ event = ActiveSupport::Notifications::Event.new(*args)
83
+ transaction_name = PerformanceInstrumentation.job_transaction_name(event.payload)
84
+ flusher.record(transaction_name: transaction_name, duration_ms: event.duration, kind: "job")
85
+ end
86
+ end
87
+
88
+ # Raw (non-ActiveJob) Sidekiq::Worker job timing, plus Sidekiq's own queue-depth/processed/
89
+ # failed/etc. gauges: a separate initializer, since unlike ActiveRecord/ActionController
90
+ # (always present in any Rails app this gem's own gemspec already implies), Sidekiq is
91
+ # genuinely optional and isn't a declared dependency of this gem at all.
92
+ initializer "forge_ops_tracker.track_sidekiq" do
93
+ next unless defined?(::Sidekiq)
94
+
95
+ require "forge_ops_tracker/integrations/sidekiq"
96
+
97
+ configuration = ForgeOpsTracker.configuration
98
+ ::Sidekiq.configure_server do |config|
99
+ config.server_middleware do |chain|
100
+ # No extra args to chain.add: Sidekiq calls Middleware.new(*args) itself for every job,
101
+ # splatting whatever was passed here; the middleware's own keyword-argument defaults
102
+ # (ForgeOpsTracker.configuration, Railtie.performance_flusher) already resolve correctly
103
+ # when called with none, and passing a keyword hash through *args here is genuinely
104
+ # ambiguous (Sidekiq's own chain.add doc example passes a literal positional Hash, not
105
+ # keyword syntax, for exactly this reason).
106
+ chain.add ForgeOpsTracker::Integrations::Sidekiq::Middleware
107
+ end
108
+ end
109
+
110
+ ForgeOpsTracker::Integrations::Sidekiq::StatsReporter.start(configuration: configuration)
111
+ end
112
+
113
+ initializer "forge_ops_tracker.track_solid_queue" do
114
+ next unless defined?(::SolidQueue)
115
+
116
+ require "forge_ops_tracker/integrations/solid_queue"
117
+ ForgeOpsTracker::Integrations::SolidQueue::StatsReporter.start(configuration: ForgeOpsTracker.configuration)
118
+ end
119
+
120
+ initializer "forge_ops_tracker.track_puma" do
121
+ next unless defined?(::Puma)
122
+
123
+ require "forge_ops_tracker/integrations/puma"
124
+ ForgeOpsTracker::Integrations::Puma::StatsReporter.start(configuration: ForgeOpsTracker.configuration)
125
+ end
126
+
127
+ # Net::HTTP is always present (Ruby's own standard library), so this always prepends; the
128
+ # actual on/off decision happens at call time inside Timing#request itself (checking
129
+ # configuration.track_performance/enabled? the same way the subscriptions above do), so
130
+ # toggling that flag after boot still takes effect immediately, the same as every other
131
+ # automatic instrumentation point in this gem.
132
+ initializer "forge_ops_tracker.track_net_http" do
133
+ require "forge_ops_tracker/integrations/net_http"
134
+ Net::HTTP.prepend(ForgeOpsTracker::Integrations::NetHTTP::Timing)
135
+ end
13
136
  end
14
137
  end
@@ -0,0 +1,85 @@
1
+ require "thread"
2
+
3
+ module ForgeOpsTracker
4
+ # Counts requests and crashes in-process (see Middleware::SessionTracking) and periodically
5
+ # flushes the totals as one small aggregate report, rather than one network call per request.
6
+ # Same "lazily start a background thread on first use, not at load time" pattern DeliveryQueue
7
+ # already uses, so each forked Puma/Passenger worker gets its own fresh thread instead of
8
+ # inheriting a dead one across fork; here it's triggered by a sleep-loop timer instead of a
9
+ # queue push, since there's no per-event signal to wait on, just a clock.
10
+ class SessionFlusher
11
+ def initialize(configuration, client: Client.new(configuration))
12
+ @configuration = configuration
13
+ @client = client
14
+ @mutex = Mutex.new
15
+ @sessions_count = 0
16
+ @crashed_sessions_count = 0
17
+ @period_started_at = Time.now.utc
18
+ @thread = nil
19
+
20
+ # Flushes whatever's already tallied on a normal process exit, so the last partial window
21
+ # (anything shorter than a full session_flush_interval) isn't silently dropped.
22
+ at_exit { flush }
23
+ end
24
+
25
+ def record_session(crashed:)
26
+ ensure_worker_started
27
+
28
+ @mutex.synchronize do
29
+ @sessions_count += 1
30
+ @crashed_sessions_count += 1 if crashed
31
+ end
32
+ end
33
+
34
+ # Snapshots and resets the in-process counters, then delivers them. A failed delivery keeps
35
+ # the counts where they are rather than resetting, so the next flush's window just grows
36
+ # instead of losing what was already tallied; there's no other copy of this data anywhere.
37
+ def flush
38
+ snapshot = nil
39
+
40
+ @mutex.synchronize do
41
+ return if @sessions_count.zero?
42
+
43
+ snapshot = {
44
+ release: configuration.release,
45
+ environment: configuration.environment.to_s,
46
+ period_started_at: @period_started_at.iso8601,
47
+ period_ended_at: Time.now.utc.iso8601,
48
+ sessions_count: @sessions_count,
49
+ crashed_sessions_count: @crashed_sessions_count
50
+ }
51
+ end
52
+
53
+ return unless snapshot && client.deliver_session_checkin(snapshot)
54
+
55
+ @mutex.synchronize do
56
+ @sessions_count = 0
57
+ @crashed_sessions_count = 0
58
+ @period_started_at = Time.now.utc
59
+ end
60
+ end
61
+
62
+ private
63
+ attr_reader :configuration, :client
64
+
65
+ def ensure_worker_started
66
+ return if @thread&.alive?
67
+
68
+ @mutex.synchronize do
69
+ return if @thread&.alive?
70
+
71
+ @thread = Thread.new { run }
72
+ @thread.abort_on_exception = false
73
+ end
74
+ end
75
+
76
+ def run
77
+ loop do
78
+ sleep configuration.session_flush_interval
79
+ flush
80
+ rescue StandardError => e
81
+ configuration.logger&.debug { "[ForgeOpsTracker] session flush thread error: #{e.class}: #{e.message}" }
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,3 +1,3 @@
1
1
  module ForgeOpsTracker
2
- VERSION = "0.2.1"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -5,6 +5,14 @@ require "forge_ops_tracker/event_builder"
5
5
  require "forge_ops_tracker/client"
6
6
  require "forge_ops_tracker/delivery_queue"
7
7
  require "forge_ops_tracker/error_subscriber"
8
+ require "forge_ops_tracker/session_flusher"
9
+ require "forge_ops_tracker/performance_flusher"
10
+ require "forge_ops_tracker/performance_instrumentation"
11
+ require "forge_ops_tracker/metric_buffer"
12
+ require "forge_ops_tracker/infrastructure_metric_buffer"
13
+ require "forge_ops_tracker/periodic_poller"
14
+ require "forge_ops_tracker/middleware/session_tracking"
15
+ require "forge_ops_tracker/middleware/user_context"
8
16
 
9
17
  module ForgeOpsTracker
10
18
  class << self
@@ -15,6 +23,50 @@ module ForgeOpsTracker
15
23
  def configure
16
24
  yield configuration
17
25
  end
26
+
27
+ # Records a named business metric (a signup, a payment, anything a customer wants to name),
28
+ # buffered and flushed periodically as a batch rather than one network call per capture (see
29
+ # MetricBuffer). value defaults to 1.0 so a bare counter-style call ("a signup happened")
30
+ # needs no argument; pass an explicit one for a metric with a real magnitude ("a $49 payment
31
+ # happened"). A no-op, same as every other capture path in this gem, when the gem isn't
32
+ # enabled (no DSN configured, or the current environment isn't in enabled_environments).
33
+ def capture_metric(name, value: 1.0)
34
+ return unless configuration.enabled?
35
+
36
+ metric_buffer.record(metric_name: name, value: value)
37
+ end
38
+
39
+ # Records one infrastructure reading (CPU/memory/disk, or anything else a customer's own
40
+ # script reads) from one of their own hosts. hostname defaults to Configuration#server_name
41
+ # (already derived from Socket.gethostname), so a script running on the box it's reporting
42
+ # about doesn't need to pass one explicitly. Same buffered-batch delivery and no-op-when-
43
+ # disabled contract as capture_metric above.
44
+ def capture_infrastructure_metric(name, value:, hostname: nil)
45
+ return unless configuration.enabled?
46
+
47
+ infrastructure_metric_buffer.record(metric_name: name, value: value, hostname: hostname || configuration.server_name)
48
+ end
49
+
50
+ # Manually attaches an affected user to whatever gets reported for the rest of this request
51
+ # (or, outside a request entirely, in a background job or a console session, for the rest of
52
+ # this thread). Sets the same thread-local ForgeOpsTracker::Middleware::UserContext's own
53
+ # Warden auto-detection uses, so this composes with it rather than being a second, separate
54
+ # mechanism: call this after that middleware ran to override its guess, or call it standalone
55
+ # in an app with no Warden at all. id/email/username are all independently optional; a call
56
+ # with none of them (or all nil) clears whatever was set.
57
+ def set_user(id: nil, email: nil, username: nil)
58
+ user = { id: id, email: email, username: username }.compact
59
+ Thread.current[:forge_ops_tracker_current_user] = user.empty? ? nil : user
60
+ end
61
+
62
+ private
63
+ def metric_buffer
64
+ @metric_buffer ||= MetricBuffer.new(configuration)
65
+ end
66
+
67
+ def infrastructure_metric_buffer
68
+ @infrastructure_metric_buffer ||= InfrastructureMetricBuffer.new(configuration)
69
+ end
18
70
  end
19
71
  end
20
72
 
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.2.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ForgeOps
@@ -23,8 +23,50 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.13'
26
- description: Hooks Rails' error reporter and reports exceptions to a private ForgeOps
27
- exception tracker instance over HTTP, without ever raising back into the host application.
26
+ - !ruby/object:Gem::Dependency
27
+ name: sidekiq
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: puma
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: webrick
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.8'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.8'
68
+ description: Hooks Rails' error reporter and reports exceptions to a ForgeOps exception
69
+ tracker instance over HTTP, without ever raising back into the host application.
28
70
  executables: []
29
71
  extensions: []
30
72
  extra_rdoc_files: []
@@ -38,8 +80,20 @@ files:
38
80
  - lib/forge_ops_tracker/delivery_queue.rb
39
81
  - lib/forge_ops_tracker/error_subscriber.rb
40
82
  - lib/forge_ops_tracker/event_builder.rb
83
+ - lib/forge_ops_tracker/infrastructure_metric_buffer.rb
84
+ - lib/forge_ops_tracker/integrations/net_http.rb
85
+ - lib/forge_ops_tracker/integrations/puma.rb
86
+ - lib/forge_ops_tracker/integrations/sidekiq.rb
87
+ - lib/forge_ops_tracker/integrations/solid_queue.rb
88
+ - lib/forge_ops_tracker/metric_buffer.rb
89
+ - lib/forge_ops_tracker/middleware/session_tracking.rb
90
+ - lib/forge_ops_tracker/middleware/user_context.rb
91
+ - lib/forge_ops_tracker/performance_flusher.rb
92
+ - lib/forge_ops_tracker/performance_instrumentation.rb
93
+ - lib/forge_ops_tracker/periodic_poller.rb
41
94
  - lib/forge_ops_tracker/pii_scrubber.rb
42
95
  - lib/forge_ops_tracker/railtie.rb
96
+ - lib/forge_ops_tracker/session_flusher.rb
43
97
  - lib/forge_ops_tracker/version.rb
44
98
  homepage: https://getforgeops.net
45
99
  licenses:
@@ -63,5 +117,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
117
  requirements: []
64
118
  rubygems_version: 4.0.11
65
119
  specification_version: 4
66
- summary: Rails exception reporting client for a self-hosted ForgeOps tracker
120
+ summary: Rails exception reporting client for a ForgeOps tracker
67
121
  test_files: []