Class: Beacon::Middleware

Inherits:
Object
  • Object
show all
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

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

#statsObject

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