Class: Hyperion::AdminListener
- Inherits:
-
Object
- Object
- Hyperion::AdminListener
- Defined in:
- lib/hyperion/admin_listener.rb
Overview
Sibling HTTP listener for admin endpoints (RFC A8). When the operator sets ‘admin.listener_port`, Hyperion spawns a small dedicated server on `127.0.0.1:<port>` that handles ONLY `/-/quit` and `/-/metrics` (Prometheus exposition). The application listener is unchanged —admin paths can stay mounted in-app simultaneously, depending on whether `AdminMiddleware` is wrapped.
**Why a sibling listener, not just middleware?** Three failure modes AdminMiddleware can’t escape on its own:
1. Misordered `Rack::Builder` middleware can disable admin (a
`use` of a custom 404 middleware in front of Hyperion's wrap).
2. Request-headers-logging middleware (`Rack::CommonLogger` derivs,
OpenTelemetry HTTP instrumentation, app-level header dumpers)
logs the `X-Hyperion-Admin-Token` value to access logs. The
sibling listener's path never goes through that pipeline.
3. Operators who don't want to manually 404 `/-/*` at the edge
proxy can simply not expose this port.
**Defence-in-depth, not a replacement for network isolation.** The bearer token still gates every request. Operators MUST keep this port on a private interface (default ‘127.0.0.1`) or behind an authenticating reverse proxy. Same `secure_match?` logic as AdminMiddleware.
**Implementation note.** Single accept thread, no Rack pipeline. We parse the request line + Authorization header by hand because:
* The two endpoints are trivial (drain via SIGTERM; render
pre-formatted Prometheus text).
* Pulling in a full Rack stack inside Hyperion to serve two
endpoints would re-introduce the misordering footgun (#1 above).
* The bytes per response are tiny — encryption / chunked encoding
/ keep-alive aren't needed.
Returns 202 + ‘“status”:“draining”` on quit, 200 + Prometheus text on metrics, 401 on bearer mismatch, 404 on anything else.
Constant Summary collapse
- PATH_QUIT =
'/-/quit'- PATH_METRICS =
'/-/metrics'- METRICS_CONTENT_TYPE =
'text/plain; version=0.0.4; charset=utf-8'- JSON_CONTENT_TYPE =
'application/json'- UNAUTHORIZED_BODY =
%({"error":"unauthorized"}\n)- NOT_FOUND_BODY =
%({"error":"not_found"}\n)- DRAINING_BODY =
%({"status":"draining"}\n)- SIGNAL_FAILED =
%({"error":"signal_failed"}\n)
Instance Attribute Summary collapse
-
#host ⇒ Object
readonly
Returns the value of attribute host.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
Instance Method Summary collapse
-
#initialize(host:, port:, token:, runtime: nil, signal_target: nil) ⇒ AdminListener
constructor
A new instance of AdminListener.
-
#start ⇒ Object
Bind + spawn the accept thread.
- #stop ⇒ Object
Constructor Details
#initialize(host:, port:, token:, runtime: nil, signal_target: nil) ⇒ AdminListener
Returns a new instance of AdminListener.
58 59 60 61 62 63 64 65 66 67 |
# File 'lib/hyperion/admin_listener.rb', line 58 def initialize(host:, port:, token:, runtime: nil, signal_target: nil) raise ArgumentError, 'admin listener token must be a non-empty String' if token.nil? || token.to_s.empty? @host = host @port = port @token = token.to_s @runtime = runtime || Hyperion::Runtime.default @signal_target = signal_target @stopped = false end |
Instance Attribute Details
#host ⇒ Object (readonly)
Returns the value of attribute host.
56 57 58 |
# File 'lib/hyperion/admin_listener.rb', line 56 def host @host end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
56 57 58 |
# File 'lib/hyperion/admin_listener.rb', line 56 def port @port end |
Instance Method Details
#start ⇒ Object
Bind + spawn the accept thread. Returns self so callers can chain ‘.start.join` or just hold the reference for `#stop`.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/hyperion/admin_listener.rb', line 71 def start @server = ::TCPServer.new(@host, @port) # Honour port: 0 (let kernel pick) — the test suite uses this so # multiple AdminListeners can coexist without port conflicts. @port = @server.addr[1] @thread = Thread.new { accept_loop } @thread.report_on_exception = false @runtime.logger.info do { message: 'admin listener started', host: @host, port: @port, paths: [PATH_QUIT, PATH_METRICS] } end self end |
#stop ⇒ Object
86 87 88 89 90 91 92 93 |
# File 'lib/hyperion/admin_listener.rb', line 86 def stop @stopped = true @server&.close @thread&.join(5) nil rescue StandardError nil end |