Module: AllStak::GlobalHandler

Defined in:
lib/allstak/global_handler.rb

Overview

Global uncaught-exception capture.

Ruby has no first-class “uncaught exception” callback, but it runs ‘at_exit` blocks during interpreter teardown — and while they run, `$!` still holds the exception that is killing the process (if any). Inspecting `$!` from inside an `at_exit` block is the idiomatic way to catch a top-level unhandled exception. We capture it, mark it unhandled (mechanism handled=false), and do a best-effort synchronous flush before the process dies.

We are deliberately conservative about WHAT counts as an unhandled termination so a clean exit (or a normal ‘exit`/`exit!`) is never reported as an error:

- `$!` must be present and be an Exception.
- SystemExit is treated as unhandled only when its status is non-zero
  (i.e. `exit(1)` / `abort`), never `exit(0)` / `exit` (clean exit).
- SignalException (e.g. Ctrl-C / SIGINT) is ignored.

Constant Summary collapse

MECHANISM =
"at_exit".freeze

Class Method Summary collapse

Class Method Details

.capture_unhandled(exc) ⇒ Object

Capture an exception as a global, unhandled event and flush synchronously. Safe to call directly as a documented integration point:

begin
  run_worker
rescue => e
  AllStak::GlobalHandler.capture_unhandled(e)
  raise
end

Also surfaced as AllStak.capture_unhandled.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/allstak/global_handler.rb', line 80

def capture_unhandled(exc)
  return nil unless AllStak.initialized?
  client = AllStak.client
  begin
    client.capture_exception(
      exc,
      metadata: {
        "mechanism" => MECHANISM,
        "handled"   => false
      }
    )
  rescue => e
    AllStak.logger&.debug("[AllStak] at_exit capture failed: #{e.class}: #{e.message}")
  ensure
    # Best-effort synchronous flush so buffered telemetry leaves the
    # process before it dies. Never raise out of an at_exit hook.
    client.flush rescue nil
  end
end

.install!(logger = nil) ⇒ Object

Install the process-wide at_exit hook. Idempotent: the actual at_exit block is registered exactly once per process, regardless of how many times this is called (reconfigure, multiple configure calls, etc.). The block reads the live client at exit time, so reconfiguration is honored without re-registering.



29
30
31
32
33
34
# File 'lib/allstak/global_handler.rb', line 29

def install!(logger = nil)
  return if @installed
  @installed = true
  logger&.debug("[AllStak] installing at_exit unhandled-exception handler")
  at_exit { run_at_exit($!) }
end

.installed?Boolean

Returns:

  • (Boolean)


36
37
38
# File 'lib/allstak/global_handler.rb', line 36

def installed?
  @installed == true
end

.reset!Object

Test seam: forget that we installed (does NOT unregister the real at_exit block — Ruby has no API for that — but lets a test drive install!/idempotency logic deterministically).



43
44
45
# File 'lib/allstak/global_handler.rb', line 43

def reset!
  @installed = false
end

.run_at_exit(exc) ⇒ Object

The body of the at_exit hook, factored out so it is directly unit testable without actually terminating the process. ‘exc` is whatever `$!` held at exit time.



50
51
52
53
54
# File 'lib/allstak/global_handler.rb', line 50

def run_at_exit(exc)
  return unless AllStak.initialized?
  return unless unhandled_termination?(exc)
  capture_unhandled(exc)
end

.unhandled_termination?(exc) ⇒ Boolean

Decide whether ‘exc` represents a genuine unhandled termination that we should report, vs. a clean/expected exit we must ignore.

Returns:

  • (Boolean)


58
59
60
61
62
63
64
65
66
67
# File 'lib/allstak/global_handler.rb', line 58

def unhandled_termination?(exc)
  return false unless exc.is_a?(Exception)
  # Ignore Ctrl-C / signal-driven teardown.
  return false if exc.is_a?(SignalException)
  # `exit`/`exit(0)` raise SystemExit with success? == true: clean exit.
  if exc.is_a?(SystemExit)
    return exc.respond_to?(:success?) ? !exc.success? : exc.status.to_i != 0
  end
  true
end