forge_ops_tracker 0.7.0 → 0.10.1

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.
@@ -0,0 +1,65 @@
1
+ require "securerandom"
2
+
3
+ module ForgeOpsTracker
4
+ # Accumulates one request's own nested call tree in-process; the tracing analog to
5
+ # BreadcrumbBuffer's per-thread trail. ForgeOpsTracker::Middleware::SpanTracing resets this to a
6
+ # fresh buffer at the start of every request, with a freshly generated trace_id and root
7
+ # span_id, and reads it back once the request is over to decide whether this trace is worth
8
+ # sending at all (see that middleware's own comment for exactly where and why).
9
+ #
10
+ # The stack exists so nesting can be reconstructed correctly even though most of what's recorded
11
+ # here (a query, an outbound HTTP call, a Redis call) only has a start+duration known *after* the
12
+ # fact, not a real "this span is now open" moment the way a manually-wrapped ForgeOpsTracker.span
13
+ # block has. Whatever's on top of the stack at the moment a leaf span is recorded is that leaf's
14
+ # real parent: the nearest currently-open span, whether that's the request's own root, or a
15
+ # service-level span a customer wrapped their own code in. The root span_id is pushed onto the
16
+ # stack immediately, at construction, specifically so every span recorded before the controller
17
+ # action itself finishes (which is every one of them, since process_action.action_controller's
18
+ # own subscription only fires once the whole action, including everything nested inside it, has
19
+ # already run) naturally nests under it without the root having to exist as a real recorded span
20
+ # first.
21
+ class SpanBuffer
22
+ attr_reader :trace_id, :root_span_id, :spans
23
+
24
+ def initialize
25
+ @trace_id = SecureRandom.hex(16)
26
+ @root_span_id = SecureRandom.hex(8)
27
+ @spans = []
28
+ @stack = [ @root_span_id ]
29
+ @root_duration_ms = nil
30
+ end
31
+
32
+ def current_parent_id
33
+ @stack.last
34
+ end
35
+
36
+ def push(span_id)
37
+ @stack.push(span_id)
38
+ end
39
+
40
+ def pop
41
+ @stack.pop
42
+ end
43
+
44
+ # root: true only for the one call that records the request's own root span (see Railtie's
45
+ # process_action.action_controller subscription): forces parent_span_id nil explicitly, since
46
+ # by the time that fires, the root's own span_id has already been popped off the stack (there's
47
+ # nothing left on it to read back as "the parent" at that point - correctly, since the root has
48
+ # no parent of its own).
49
+ def record(span_id:, name:, kind:, started_at:, duration_ms:, data: {}, root: false)
50
+ parent_span_id = root ? nil : current_parent_id
51
+ spans << {
52
+ span_id: span_id, parent_span_id: parent_span_id, name: name, kind: kind,
53
+ started_at: started_at.utc.iso8601(3), duration_ms: duration_ms, data: data
54
+ }
55
+ @root_duration_ms = duration_ms if root
56
+ end
57
+
58
+ # nil (never sent) until the root span has actually been recorded; a request that somehow never
59
+ # fires process_action.action_controller at all (a request Rails itself never routes) has no
60
+ # duration to compare against a threshold, so it's never mistakenly treated as "slow."
61
+ def slow?(threshold_ms)
62
+ @root_duration_ms && @root_duration_ms >= threshold_ms
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,61 @@
1
+ require "thread"
2
+
3
+ module ForgeOpsTracker
4
+ # Same shape and reasoning as DeliveryQueue: a small in-process background thread + bounded
5
+ # queue, so delivering a captured trace never adds latency to the very request it was just
6
+ # measuring - the one thing that would be actively counterproductive here, blocking an already-
7
+ # slow request even longer just to report that it was slow. One whole trace (a trace_id plus
8
+ # every span belonging to it) is one queue entry, delivered as one client call, not batched
9
+ # together with other traces the way PerformanceFlusher/FailureEventBuffer batch over a time
10
+ # window: a trace is already a complete, immediately-relevant unit the moment a request
11
+ # finishes, so it's delivered promptly rather than held for up to a full flush interval.
12
+ class SpanQueue
13
+ def initialize(configuration, client: Client.new(configuration))
14
+ @configuration = configuration
15
+ @client = client
16
+ @queue = SizedQueue.new(configuration.queue_size)
17
+ @mutex = Mutex.new
18
+ @thread = nil
19
+ end
20
+
21
+ # Dropping a whole trace silently (never blocking the caller) if the queue is already full,
22
+ # the same backpressure-must-never-reach-the-host-app posture DeliveryQueue#push already
23
+ # documents; a burst of several slow requests at once must never stall a request that's
24
+ # already been identified as slow even further.
25
+ def push(trace_id:, spans:)
26
+ ensure_worker_started
27
+ @queue.push({ trace_id: trace_id, spans: spans }, true)
28
+ true
29
+ rescue ThreadError
30
+ log { "span delivery queue full, dropping trace" }
31
+ false
32
+ end
33
+
34
+ private
35
+ attr_reader :configuration, :client
36
+
37
+ def ensure_worker_started
38
+ return if @thread&.alive?
39
+
40
+ @mutex.synchronize do
41
+ return if @thread&.alive?
42
+
43
+ @thread = Thread.new { run }
44
+ @thread.abort_on_exception = false
45
+ end
46
+ end
47
+
48
+ def run
49
+ loop do
50
+ entry = @queue.pop
51
+ client.deliver_spans(trace_id: entry[:trace_id], spans: entry[:spans])
52
+ rescue StandardError => e
53
+ log { "span delivery thread error: #{e.class}: #{e.message}" }
54
+ end
55
+ end
56
+
57
+ def log
58
+ configuration.logger&.debug { "[ForgeOpsTracker] #{yield}" }
59
+ end
60
+ end
61
+ end
@@ -1,3 +1,3 @@
1
1
  module ForgeOpsTracker
2
- VERSION = "0.7.0"
2
+ VERSION = "0.10.1"
3
3
  end
@@ -1,3 +1,4 @@
1
+ require "securerandom"
1
2
  require "forge_ops_tracker/version"
2
3
  require "forge_ops_tracker/configuration"
3
4
  require "forge_ops_tracker/pii_scrubber"
@@ -6,13 +7,20 @@ require "forge_ops_tracker/client"
6
7
  require "forge_ops_tracker/delivery_queue"
7
8
  require "forge_ops_tracker/error_subscriber"
8
9
  require "forge_ops_tracker/session_flusher"
10
+ require "forge_ops_tracker/histogram_bucketer"
9
11
  require "forge_ops_tracker/performance_flusher"
10
12
  require "forge_ops_tracker/performance_instrumentation"
11
13
  require "forge_ops_tracker/metric_buffer"
12
14
  require "forge_ops_tracker/infrastructure_metric_buffer"
15
+ require "forge_ops_tracker/failure_event_buffer"
13
16
  require "forge_ops_tracker/periodic_poller"
17
+ require "forge_ops_tracker/breadcrumb_buffer"
18
+ require "forge_ops_tracker/span_buffer"
19
+ require "forge_ops_tracker/span_queue"
14
20
  require "forge_ops_tracker/middleware/session_tracking"
15
21
  require "forge_ops_tracker/middleware/user_context"
22
+ require "forge_ops_tracker/middleware/breadcrumb_context"
23
+ require "forge_ops_tracker/middleware/span_tracing"
16
24
 
17
25
  module ForgeOpsTracker
18
26
  class << self
@@ -59,6 +67,54 @@ module ForgeOpsTracker
59
67
  Thread.current[:forge_ops_tracker_current_user] = user.empty? ? nil : user
60
68
  end
61
69
 
70
+ # Adds one breadcrumb to the current request's trail (or, outside a request entirely, a
71
+ # background job or console session's own trail for the rest of this thread: see
72
+ # ForgeOpsTracker::Middleware::BreadcrumbContext for how a request gets its own buffer to start
73
+ # with). Lazily creates a buffer on this thread if BreadcrumbContext never ran (Sidekiq/Solid
74
+ # Queue jobs don't go through Rack middleware at all, an IRB/rails console session even less
75
+ # so), the same "works standalone, no specific setup required" shape ForgeOpsTracker.set_user
76
+ # already has. Works whether or not track_breadcrumbs is on: that flag only gates the automatic
77
+ # sql.active_record/process_action.action_controller/Net::HTTP sources Railtie installs, never
78
+ # this manual call.
79
+ def add_breadcrumb(message, category: "custom", level: "info", data: {})
80
+ buffer = (Thread.current[:forge_ops_tracker_breadcrumbs] ||= BreadcrumbBuffer.new(configuration))
81
+ buffer.add(category: category, message: message, level: level, data: data)
82
+ end
83
+
84
+ # Wraps a customer's own service-level code as one named, nested span in the current request's
85
+ # trace (e.g. `ForgeOpsTracker.span("PaymentService#charge") { ... }`), so its database/Redis/
86
+ # outbound-HTTP calls show up nested underneath it in the waterfall rather than appearing to
87
+ # hang directly off the controller root - there's no automatic way to detect "this is a
88
+ # logically distinct service layer" the way a SQL query or an HTTP request already has a real
89
+ # instrumentation hook to detect, so this is deliberately explicit, opt-in, by hand, unlike
90
+ # every other span kind this gem records automatically.
91
+ #
92
+ # A plain pass-through (still runs the block, records nothing) with no error and no special
93
+ # handling whenever there's no current request trace to nest under at all: outside a request
94
+ # entirely (a background job, a console session), when track_tracing is off, or when the gem
95
+ # isn't enabled - the same "never break the host app's own code path" posture every other
96
+ # capture method in this gem already takes. Unlike add_breadcrumb, this does NOT lazily create
97
+ # a standalone buffer for itself: a lone span with no request-level trace around it, and no
98
+ # middleware left running to ever flush it, would just accumulate in this thread's memory
99
+ # forever with nothing to ever send it, which is worse than never recording it at all.
100
+ def span(name, kind: "service", data: {})
101
+ return yield unless configuration.enabled? && configuration.track_tracing
102
+
103
+ buffer = Thread.current[:forge_ops_tracker_spans]
104
+ return yield unless buffer
105
+
106
+ span_id = SecureRandom.hex(8)
107
+ started_at = Time.now.utc
108
+ buffer.push(span_id)
109
+ begin
110
+ yield
111
+ ensure
112
+ buffer.pop
113
+ duration_ms = (Time.now.utc - started_at) * 1000.0
114
+ buffer.record(span_id: span_id, name: name, kind: kind, started_at: started_at, duration_ms: duration_ms, data: data)
115
+ end
116
+ end
117
+
62
118
  private
63
119
  def metric_buffer
64
120
  @metric_buffer ||= MetricBuffer.new(configuration)
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.7.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ForgeOps
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.8'
68
+ - !ruby/object:Gem::Dependency
69
+ name: redis-client
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.30'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.30'
68
82
  description: Hooks Rails' error reporter and reports exceptions to a ForgeOps exception
69
83
  tracker instance over HTTP, without ever raising back into the host application.
70
84
  executables: []
@@ -75,18 +89,25 @@ files:
75
89
  - LICENSE.txt
76
90
  - README.md
77
91
  - lib/forge_ops_tracker.rb
92
+ - lib/forge_ops_tracker/breadcrumb_buffer.rb
78
93
  - lib/forge_ops_tracker/client.rb
79
94
  - lib/forge_ops_tracker/configuration.rb
80
95
  - lib/forge_ops_tracker/delivery_queue.rb
81
96
  - lib/forge_ops_tracker/error_subscriber.rb
82
97
  - lib/forge_ops_tracker/event_builder.rb
98
+ - lib/forge_ops_tracker/failure_event_buffer.rb
99
+ - lib/forge_ops_tracker/histogram_bucketer.rb
83
100
  - lib/forge_ops_tracker/infrastructure_metric_buffer.rb
101
+ - lib/forge_ops_tracker/integrations/active_record_pool.rb
84
102
  - lib/forge_ops_tracker/integrations/net_http.rb
85
103
  - lib/forge_ops_tracker/integrations/puma.rb
104
+ - lib/forge_ops_tracker/integrations/redis_client.rb
86
105
  - lib/forge_ops_tracker/integrations/sidekiq.rb
87
106
  - lib/forge_ops_tracker/integrations/solid_queue.rb
88
107
  - lib/forge_ops_tracker/metric_buffer.rb
108
+ - lib/forge_ops_tracker/middleware/breadcrumb_context.rb
89
109
  - lib/forge_ops_tracker/middleware/session_tracking.rb
110
+ - lib/forge_ops_tracker/middleware/span_tracing.rb
90
111
  - lib/forge_ops_tracker/middleware/user_context.rb
91
112
  - lib/forge_ops_tracker/performance_flusher.rb
92
113
  - lib/forge_ops_tracker/performance_instrumentation.rb
@@ -94,6 +115,8 @@ files:
94
115
  - lib/forge_ops_tracker/pii_scrubber.rb
95
116
  - lib/forge_ops_tracker/railtie.rb
96
117
  - lib/forge_ops_tracker/session_flusher.rb
118
+ - lib/forge_ops_tracker/span_buffer.rb
119
+ - lib/forge_ops_tracker/span_queue.rb
97
120
  - lib/forge_ops_tracker/version.rb
98
121
  homepage: https://getforgeops.net
99
122
  licenses: