Class: Beacon::Middleware
- Inherits:
-
Object
- Object
- Beacon::Middleware
- Defined in:
- lib/beacon/middleware.rb
Overview
Rack middleware that captures perf and errors on the host’s hot path.
Hot-path discipline (see .doc/definition/05-clients.md):
- capture monotonic start
- call the app
- build a small Hash event
- push to the sink (non-blocking)
- return
No JSON encoding, no I/O, no allocation of large buffers. Path normalization is cached per (method, path) so steady-state requests avoid the regex work.
The middleware is rescue-all: any exception from Beacon’s own code is logged and swallowed. Host exceptions are recorded as errors and then re-raised so the host’s normal error handling continues to run.
Constant Summary collapse
- LANGUAGE =
"ruby".freeze
- STACK_TRACE_MAX_BYTES =
Upper bound for the stack_trace property. The HTTP API limits any single property value to 16 KB — a 500-frame backtrace easily exceeds that. We keep as many leading frames as fit and append a truncation marker so readers see the trace was clipped.
16 * 1024
- STACK_TRACE_TRUNCATED_SUFFIX =
"\n… (truncated)".freeze
- NON_APP_PATH_PATTERNS =
Paths we treat as “not app code” when picking the first app frame. The first pattern catches gem vendor dirs; the second catches Ruby stdlib paths like ‘/ruby/3.4.0/` or `/ruby-3.4.4/`, without accidentally matching host directories like `clients/ruby/…` that merely contain the word “ruby”.
[ %r{/gems/}, %r{/ruby[-/]\d}, ].freeze
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app, sink: nil, config: Beacon.config, logger: nil) ⇒ Middleware
constructor
A new instance of Middleware.
-
#stats ⇒ Object
Public stats surface for tests and Beacon.stats.
Constructor Details
#initialize(app, sink: nil, config: Beacon.config, logger: nil) ⇒ Middleware
Returns a new instance of Middleware.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/beacon/middleware.rb', line 41 def initialize(app, sink: nil, config: Beacon.config, logger: nil) sink ||= Beacon.client @app = app @sink = sink @config = config @logger = logger @app_root = config.app_root.to_s.chomp("/").freeze @enabled = config.enabled? @capture_perf = config.pillar?(:perf) @capture_errors = config.pillar?(:errors) @capture_ambient = config.ambient @enrich_block = config.enrich_context @enrich_warned = false # Pre-built shared context. Frozen so the same Hash is referenced from # every event without per-request allocation. @base_context = { environment: config.environment, deploy_sha: config.deploy_sha, language: LANGUAGE, }.compact.freeze # Per-(method,path) name cache — bounded LRU. Replaces the old # two-level Hash whose eviction check only counted top-level method # keys and would have let a bot probing distinct URLs OOM the worker. @name_cache = Beacon::LRU.new(max: config.cache_size) # Fingerprint -> last full-stack send time (monotonic seconds). # Bounded LRU so a misbehaving error class with high-cardinality # fingerprints can't grow the throttle map without bound. @stack_seen = Beacon::LRU.new(max: config.cache_size) end |
Instance Method Details
#call(env) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/beacon/middleware.rb', line 86 def call(env) # Kill-switch fast path: a disabled middleware is a pure passthrough # with zero allocations beyond this branch. This is what makes # BEACON_DISABLED=1 truly free at request time. return @app.call(env) unless @enabled start_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) # Compute enrichment once per request — the result is shared across # perf, ambient, and error events to avoid double-invoking the user's # block (which may touch Warden/session/DB). dims = enrich_dimensions(env) if @enrich_block begin status, headers, body = @app.call(env) rescue Exception => host_error # rubocop:disable Lint/RescueException capture_perf(env, 500, start_ns, dims) if @capture_perf capture_ambient(env, 500, start_ns, dims) if @capture_ambient capture_error(env, host_error, dims) if @capture_errors raise end capture_perf(env, status, start_ns, dims) if @capture_perf capture_ambient(env, status, start_ns, dims) if @capture_ambient [status, headers, body] end |
#stats ⇒ Object
Public stats surface for tests and Beacon.stats. Returns only the counters that are load-bearing for cache-bound debugging: name_cache_size and stack_seen_size. Both are bounded by Configuration#cache_size.
79 80 81 82 83 84 |
# File 'lib/beacon/middleware.rb', line 79 def stats { name_cache_size: @name_cache.size, stack_seen_size: @stack_seen.size, } end |