hyperion-rb 1.6.2 → 2.11.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4768 -0
- data/README.md +222 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +499 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +618 -19
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
data/lib/hyperion/metrics.rb
CHANGED
|
@@ -29,6 +29,23 @@ module Hyperion
|
|
|
29
29
|
# Public API:
|
|
30
30
|
# Hyperion.stats -> Hash with all current values across all threads.
|
|
31
31
|
class Metrics
|
|
32
|
+
class << self
|
|
33
|
+
# 2.4-C — process-wide default PathTemplater. Read by Connection at
|
|
34
|
+
# construction; written by the boot path (Worker / single-mode CLI)
|
|
35
|
+
# from `Hyperion::Config#metrics.path_templater`. Per-Connection
|
|
36
|
+
# override stays available via the `path_templater:` kwarg for
|
|
37
|
+
# specs and library users that build a Connection manually.
|
|
38
|
+
attr_writer :default_path_templater
|
|
39
|
+
|
|
40
|
+
def default_path_templater
|
|
41
|
+
@default_path_templater ||= PathTemplater.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset_default_path_templater!
|
|
45
|
+
@default_path_templater = nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
32
49
|
def initialize
|
|
33
50
|
# Direct list of every per-thread counters Hash ever allocated through
|
|
34
51
|
# this Metrics instance. We hold the Hash refs ourselves (instead of
|
|
@@ -41,6 +58,27 @@ module Hyperion
|
|
|
41
58
|
# Per-instance thread-local key so spec runs that build fresh Metrics
|
|
42
59
|
# objects don't share state across examples.
|
|
43
60
|
@thread_key = :"__hyperion_metrics_#{object_id}__"
|
|
61
|
+
|
|
62
|
+
# 2.4-C — observability enrichment. Histograms and gauges live as
|
|
63
|
+
# separate keyed structures (vs counters) because the wire format
|
|
64
|
+
# is different (per-bucket cumulative counts + sum/count for
|
|
65
|
+
# histograms; a single instantaneous reading for gauges). Both are
|
|
66
|
+
# mutex-guarded — these are scrape-rate operations (one observe per
|
|
67
|
+
# request, one set per worker boot/shutdown), not per-syscall.
|
|
68
|
+
#
|
|
69
|
+
# Histograms: `{ name => { labels_tuple_array => HistogramAccumulator } }`.
|
|
70
|
+
# Gauges: `{ name => { labels_tuple_array => Float } }`.
|
|
71
|
+
# `labels_tuple_array` is a frozen Array<String> of label values
|
|
72
|
+
# (stable order, supplied by the observer); it doubles as the Hash
|
|
73
|
+
# key for cheap O(1) lookup.
|
|
74
|
+
@histograms = {}
|
|
75
|
+
@histograms_meta = {} # name => { buckets:, label_keys: }
|
|
76
|
+
@gauges = {}
|
|
77
|
+
@gauges_meta = {} # name => { label_keys: }
|
|
78
|
+
@hg_mutex = Mutex.new
|
|
79
|
+
# Snapshot block hooks for gauges whose value is read on demand
|
|
80
|
+
# (ThreadPool queue depth, etc.). `{ name => { labels_tuple => Proc } }`.
|
|
81
|
+
@gauge_blocks = {}
|
|
44
82
|
end
|
|
45
83
|
|
|
46
84
|
# Hot path: one thread-variable lookup + one hash op. No mutex on the
|
|
@@ -90,6 +128,222 @@ module Hyperion
|
|
|
90
128
|
@counters_mutex.synchronize do
|
|
91
129
|
@thread_counters.each(&:clear)
|
|
92
130
|
end
|
|
131
|
+
@hg_mutex.synchronize do
|
|
132
|
+
@histograms.each_value(&:clear)
|
|
133
|
+
@gauges.each_value(&:clear)
|
|
134
|
+
@gauge_blocks.each_value(&:clear)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# ---- 2.4-C histogram + gauge API ---------------------------------
|
|
139
|
+
|
|
140
|
+
# Register a histogram family. Idempotent — re-registering with the
|
|
141
|
+
# same buckets/label_keys is a no-op; mismatched re-register raises
|
|
142
|
+
# so a typo surfaces at boot rather than corrupting the scrape output.
|
|
143
|
+
def register_histogram(name, buckets:, label_keys: [])
|
|
144
|
+
@hg_mutex.synchronize do
|
|
145
|
+
if (existing = @histograms_meta[name])
|
|
146
|
+
unless existing[:buckets] == buckets && existing[:label_keys] == label_keys
|
|
147
|
+
raise ArgumentError,
|
|
148
|
+
"histogram #{name.inspect} re-registered with different shape " \
|
|
149
|
+
"(was buckets=#{existing[:buckets]} labels=#{existing[:label_keys]}; " \
|
|
150
|
+
"now buckets=#{buckets} labels=#{label_keys})"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
@histograms_meta[name] = { buckets: buckets.dup.freeze, label_keys: label_keys.dup.freeze }
|
|
156
|
+
@histograms[name] = {}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Observe `value` on a previously-registered histogram. `label_values`
|
|
161
|
+
# MUST be supplied in the same order as `label_keys` at registration.
|
|
162
|
+
# The hot path: one Hash lookup, one accumulator update under a mutex.
|
|
163
|
+
# Allocation footprint per observe: zero on the cached-key path
|
|
164
|
+
# (same labels seen before); one frozen Array on first observation
|
|
165
|
+
# for a given label-set.
|
|
166
|
+
def observe_histogram(name, value, label_values = EMPTY_LABELS)
|
|
167
|
+
@hg_mutex.synchronize do
|
|
168
|
+
meta = @histograms_meta[name]
|
|
169
|
+
return unless meta # silently skip unregistered observations
|
|
170
|
+
|
|
171
|
+
family = @histograms[name]
|
|
172
|
+
accum = family[label_values]
|
|
173
|
+
unless accum
|
|
174
|
+
accum = HistogramAccumulator.new(meta[:buckets])
|
|
175
|
+
# Freeze the label tuple so future identical-content tuples
|
|
176
|
+
# hash to the same bucket — but we keep the original ref
|
|
177
|
+
# provided by the caller as the canonical key so subsequent
|
|
178
|
+
# observes with the same Array bypass the freeze step.
|
|
179
|
+
family[label_values.frozen? ? label_values : label_values.dup.freeze] = accum
|
|
180
|
+
end
|
|
181
|
+
accum.observe(value)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Set a gauge value. `label_values` follows the same convention as
|
|
186
|
+
# `observe_histogram`. Pass a block to register a callback that's
|
|
187
|
+
# evaluated lazily at snapshot time (ThreadPool queue depth, etc.) —
|
|
188
|
+
# the callback's return value is the gauge's current reading.
|
|
189
|
+
def set_gauge(name, value = nil, label_values = EMPTY_LABELS, &block)
|
|
190
|
+
@hg_mutex.synchronize do
|
|
191
|
+
@gauges_meta[name] ||= { label_keys: [].freeze }
|
|
192
|
+
if block
|
|
193
|
+
(@gauge_blocks[name] ||= {})[label_values.frozen? ? label_values : label_values.dup.freeze] = block
|
|
194
|
+
else
|
|
195
|
+
(@gauges[name] ||= {})[label_values.frozen? ? label_values : label_values.dup.freeze] = value.to_f
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Increment a gauge by `delta` (default 1). Used for kTLS active
|
|
201
|
+
# connections, etc. — paired with `decrement_gauge` on close.
|
|
202
|
+
def increment_gauge(name, label_values = EMPTY_LABELS, delta = 1)
|
|
203
|
+
@hg_mutex.synchronize do
|
|
204
|
+
@gauges_meta[name] ||= { label_keys: [].freeze }
|
|
205
|
+
family = (@gauges[name] ||= {})
|
|
206
|
+
key = label_values.frozen? ? label_values : label_values.dup.freeze
|
|
207
|
+
family[key] = (family[key] || 0.0) + delta.to_f
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def decrement_gauge(name, label_values = EMPTY_LABELS, delta = 1)
|
|
212
|
+
increment_gauge(name, label_values, -delta)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Register that a histogram/gauge family exists with this label
|
|
216
|
+
# ordering. The PrometheusExporter calls `histogram_meta` /
|
|
217
|
+
# `gauge_meta` at scrape time to build the HELP/TYPE preamble.
|
|
218
|
+
def histogram_meta(name)
|
|
219
|
+
@hg_mutex.synchronize { @histograms_meta[name]&.dup }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def gauge_meta(name)
|
|
223
|
+
@hg_mutex.synchronize { @gauges_meta[name]&.dup }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Snapshot helpers — read-only views of the current histogram /
|
|
227
|
+
# gauge state. The exporter uses these to render the scrape body.
|
|
228
|
+
def histogram_snapshot
|
|
229
|
+
out = {}
|
|
230
|
+
@hg_mutex.synchronize do
|
|
231
|
+
@histograms.each do |name, family|
|
|
232
|
+
per_labels = {}
|
|
233
|
+
family.each { |labels, accum| per_labels[labels] = accum.snapshot }
|
|
234
|
+
out[name] = { meta: @histograms_meta[name], series: per_labels }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
out
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def gauge_snapshot
|
|
241
|
+
out = {}
|
|
242
|
+
@hg_mutex.synchronize do
|
|
243
|
+
names = (@gauges.keys + @gauge_blocks.keys).uniq
|
|
244
|
+
names.each do |name|
|
|
245
|
+
per_labels = {}
|
|
246
|
+
@gauges[name]&.each { |labels, value| per_labels[labels] = value.to_f }
|
|
247
|
+
@gauge_blocks[name]&.each do |labels, block|
|
|
248
|
+
# Block-evaluated gauges read live state at scrape time. We
|
|
249
|
+
# release the mutex around the block call to avoid holding
|
|
250
|
+
# while user code runs, BUT we currently hold @hg_mutex —
|
|
251
|
+
# the contract is that the block is short and side-effect-
|
|
252
|
+
# free (e.g., reads ThreadPool#queue_size). That's the only
|
|
253
|
+
# use case we wire today; document if extended.
|
|
254
|
+
per_labels[labels] = block.call.to_f
|
|
255
|
+
rescue StandardError
|
|
256
|
+
# Snapshot must never raise — a misbehaving block degrades
|
|
257
|
+
# to "no reading" rather than a 500 on /-/metrics.
|
|
258
|
+
next
|
|
259
|
+
end
|
|
260
|
+
out[name] = { meta: @gauges_meta[name] || { label_keys: [].freeze }, series: per_labels }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
out
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Frozen empty Array used as the default label tuple. Reused across
|
|
267
|
+
# all label-less observations so we don't allocate a fresh `[]` per
|
|
268
|
+
# scrape — keeps hot-path work allocation-free for the un-labeled
|
|
269
|
+
# gauge/histogram families.
|
|
270
|
+
EMPTY_LABELS = [].freeze
|
|
271
|
+
|
|
272
|
+
# Labeled counter — separate from the legacy thread-local counter
|
|
273
|
+
# surface (which is unlabeled and per-thread for hot-path
|
|
274
|
+
# contention-free increments). Labeled counters take a mutex per
|
|
275
|
+
# increment, but they're called from low-rate paths (per-conn
|
|
276
|
+
# rejection ~ kHz worst case, vs M+req/s on the unlabeled side)
|
|
277
|
+
# so the contention cost is invisible.
|
|
278
|
+
def increment_labeled_counter(name, label_values = EMPTY_LABELS, by = 1)
|
|
279
|
+
@hg_mutex.synchronize do
|
|
280
|
+
@labeled_counters_meta ||= {}
|
|
281
|
+
@labeled_counters_meta[name] ||= { label_keys: [].freeze }
|
|
282
|
+
@labeled_counters ||= {}
|
|
283
|
+
family = (@labeled_counters[name] ||= {})
|
|
284
|
+
key = label_values.frozen? ? label_values : label_values.dup.freeze
|
|
285
|
+
family[key] = (family[key] || 0) + by
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def register_labeled_counter(name, label_keys: [])
|
|
290
|
+
@hg_mutex.synchronize do
|
|
291
|
+
@labeled_counters_meta ||= {}
|
|
292
|
+
@labeled_counters_meta[name] = { label_keys: label_keys.dup.freeze }
|
|
293
|
+
@labeled_counters ||= {}
|
|
294
|
+
@labeled_counters[name] ||= {}
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def labeled_counter_snapshot
|
|
299
|
+
out = {}
|
|
300
|
+
@hg_mutex.synchronize do
|
|
301
|
+
(@labeled_counters || {}).each do |name, family|
|
|
302
|
+
per_labels = {}
|
|
303
|
+
family.each { |labels, count| per_labels[labels] = count }
|
|
304
|
+
meta = (@labeled_counters_meta || {})[name] || { label_keys: [].freeze }
|
|
305
|
+
out[name] = { meta: meta, series: per_labels }
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
out
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Per-(name, labels) histogram accumulator. Fixed-size Integer Array
|
|
312
|
+
# of bucket counters + scalar sum/count. Cumulative bucket semantics
|
|
313
|
+
# match Prometheus client convention: bucket[i] counts observations
|
|
314
|
+
# whose value <= bucket_edges[i], and the implicit `+Inf` bucket is
|
|
315
|
+
# `count` itself. The exporter writes the +Inf bucket as the total
|
|
316
|
+
# count plus a `le="+Inf"` line per Prometheus text format.
|
|
317
|
+
class HistogramAccumulator
|
|
318
|
+
attr_reader :buckets, :counts, :sum, :count
|
|
319
|
+
|
|
320
|
+
def initialize(buckets)
|
|
321
|
+
@buckets = buckets.freeze
|
|
322
|
+
@counts = Array.new(buckets.size, 0)
|
|
323
|
+
@sum = 0.0
|
|
324
|
+
@count = 0
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Walk the buckets linearly. For 7 buckets (the default request-
|
|
328
|
+
# duration set) this is faster than binary search; for any
|
|
329
|
+
# reasonable bucket count (< 30) the constant factor wins. Mutex-
|
|
330
|
+
# guarded by the caller (Metrics#observe_histogram).
|
|
331
|
+
def observe(value)
|
|
332
|
+
v = value.to_f
|
|
333
|
+
@sum += v
|
|
334
|
+
@count += 1
|
|
335
|
+
i = 0
|
|
336
|
+
len = @buckets.length
|
|
337
|
+
while i < len
|
|
338
|
+
@counts[i] += 1 if v <= @buckets[i]
|
|
339
|
+
i += 1
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def snapshot
|
|
344
|
+
# Return a new struct so callers don't see a live, mutating ref.
|
|
345
|
+
{ buckets: @buckets, counts: @counts.dup, sum: @sum, count: @count }
|
|
346
|
+
end
|
|
93
347
|
end
|
|
94
348
|
|
|
95
349
|
private
|
|
@@ -102,3 +356,5 @@ module Hyperion
|
|
|
102
356
|
end
|
|
103
357
|
end
|
|
104
358
|
end
|
|
359
|
+
|
|
360
|
+
require_relative 'metrics/path_templater'
|
|
@@ -46,6 +46,37 @@ module Hyperion
|
|
|
46
46
|
STATUS_FAMILY_NAME = 'hyperion_responses_status_total'
|
|
47
47
|
STATUS_FAMILY_HELP = 'Responses by HTTP status code'
|
|
48
48
|
|
|
49
|
+
# 2.4-C: curated HELP/TYPE preamble for the new histogram + gauge +
|
|
50
|
+
# labeled-counter families. Looking up by name keeps the rendered
|
|
51
|
+
# scrape body human-friendly even when the caller registered the
|
|
52
|
+
# family from a deep code path with no docstring.
|
|
53
|
+
METRIC_DOCS = {
|
|
54
|
+
hyperion_request_duration_seconds: {
|
|
55
|
+
help: 'HTTP request duration in seconds, by route template + method + status class',
|
|
56
|
+
type: 'histogram'
|
|
57
|
+
},
|
|
58
|
+
hyperion_websocket_deflate_ratio: {
|
|
59
|
+
help: 'WebSocket permessage-deflate compression ratio (original_bytes / compressed_bytes)',
|
|
60
|
+
type: 'histogram'
|
|
61
|
+
},
|
|
62
|
+
hyperion_per_conn_rejections_total: {
|
|
63
|
+
help: 'Per-connection in-flight cap rejections (503 + Retry-After), by worker',
|
|
64
|
+
type: 'counter'
|
|
65
|
+
},
|
|
66
|
+
hyperion_tls_ktls_active_connections: {
|
|
67
|
+
help: 'Active TLS connections currently driven by kernel TLS_TX, by worker',
|
|
68
|
+
type: 'gauge'
|
|
69
|
+
},
|
|
70
|
+
hyperion_io_uring_workers_active: {
|
|
71
|
+
help: 'Whether io_uring accept policy is active for this worker (1 = active, 0 = epoll)',
|
|
72
|
+
type: 'gauge'
|
|
73
|
+
},
|
|
74
|
+
hyperion_threadpool_queue_depth: {
|
|
75
|
+
help: 'In-flight count in the worker ThreadPool inbox (snapshot at scrape time)',
|
|
76
|
+
type: 'gauge'
|
|
77
|
+
}
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
49
80
|
def render(stats)
|
|
50
81
|
buf = +''
|
|
51
82
|
grouped_status = {}
|
|
@@ -86,6 +117,125 @@ module Hyperion
|
|
|
86
117
|
buf
|
|
87
118
|
end
|
|
88
119
|
|
|
120
|
+
# 2.4-C — render histograms, gauges, and labeled counters from a live
|
|
121
|
+
# Metrics instance. Called by AdminMiddleware in addition to `render`
|
|
122
|
+
# so /-/metrics surfaces the full 2.4-C observability surface in one
|
|
123
|
+
# scrape body. Order: legacy counters first (existing render), then
|
|
124
|
+
# histograms, gauges, labeled counters — each curated families first,
|
|
125
|
+
# auto-exported last, alphabetical within each section so scrape
|
|
126
|
+
# diffs stay stable.
|
|
127
|
+
def render_full(metrics_sink)
|
|
128
|
+
buf = +''
|
|
129
|
+
buf << render(metrics_sink.snapshot)
|
|
130
|
+
buf << render_histograms(metrics_sink.histogram_snapshot)
|
|
131
|
+
buf << render_gauges(metrics_sink.gauge_snapshot)
|
|
132
|
+
buf << render_labeled_counters(metrics_sink.labeled_counter_snapshot)
|
|
133
|
+
buf
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def render_histograms(snap)
|
|
137
|
+
buf = +''
|
|
138
|
+
snap.each do |name, payload|
|
|
139
|
+
meta = payload[:meta]
|
|
140
|
+
series = payload[:series]
|
|
141
|
+
next if meta.nil?
|
|
142
|
+
|
|
143
|
+
doc = METRIC_DOCS[name] || { help: 'Hyperion histogram', type: 'histogram' }
|
|
144
|
+
buf << "# HELP #{name} #{doc[:help]}\n"
|
|
145
|
+
buf << "# TYPE #{name} histogram\n"
|
|
146
|
+
series.each do |label_values, snap_data|
|
|
147
|
+
render_histogram_series(buf, name, meta[:label_keys], label_values, snap_data)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
buf
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def render_gauges(snap)
|
|
154
|
+
buf = +''
|
|
155
|
+
snap.each do |name, payload|
|
|
156
|
+
series = payload[:series]
|
|
157
|
+
meta = payload[:meta] || { label_keys: [].freeze }
|
|
158
|
+
doc = METRIC_DOCS[name] || { help: 'Hyperion gauge', type: 'gauge' }
|
|
159
|
+
buf << "# HELP #{name} #{doc[:help]}\n"
|
|
160
|
+
buf << "# TYPE #{name} gauge\n"
|
|
161
|
+
series.each do |label_values, value|
|
|
162
|
+
buf << render_labeled_value(name, meta[:label_keys], label_values, value)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
buf
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def render_labeled_counters(snap)
|
|
169
|
+
buf = +''
|
|
170
|
+
snap.each do |name, payload|
|
|
171
|
+
series = payload[:series]
|
|
172
|
+
meta = payload[:meta] || { label_keys: [].freeze }
|
|
173
|
+
doc = METRIC_DOCS[name] || { help: 'Hyperion labeled counter', type: 'counter' }
|
|
174
|
+
buf << "# HELP #{name} #{doc[:help]}\n"
|
|
175
|
+
buf << "# TYPE #{name} counter\n"
|
|
176
|
+
series.each do |label_values, value|
|
|
177
|
+
buf << render_labeled_value(name, meta[:label_keys], label_values, value)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
buf
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def render_histogram_series(buf, name, label_keys, label_values, snap_data)
|
|
184
|
+
buckets = snap_data[:buckets]
|
|
185
|
+
counts = snap_data[:counts]
|
|
186
|
+
label_keys ||= []
|
|
187
|
+
label_keys = label_keys.size != label_values.size ? %w[method path status][0, label_values.size] : label_keys
|
|
188
|
+
base_pairs = label_keys.zip(label_values)
|
|
189
|
+
buckets.each_with_index do |edge, idx|
|
|
190
|
+
pairs = base_pairs + [['le', format_float(edge)]]
|
|
191
|
+
buf << "#{name}_bucket#{labels_block(pairs)} #{counts[idx]}\n"
|
|
192
|
+
end
|
|
193
|
+
pairs_inf = base_pairs + [['le', '+Inf']]
|
|
194
|
+
buf << "#{name}_bucket#{labels_block(pairs_inf)} #{snap_data[:count]}\n"
|
|
195
|
+
buf << "#{name}_sum#{labels_block(base_pairs)} #{snap_data[:sum]}\n"
|
|
196
|
+
buf << "#{name}_count#{labels_block(base_pairs)} #{snap_data[:count]}\n"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def render_labeled_value(name, label_keys, label_values, value)
|
|
200
|
+
label_keys ||= []
|
|
201
|
+
label_keys = label_keys.size == label_values.size ? label_keys : default_label_keys(label_values.size)
|
|
202
|
+
pairs = label_keys.zip(label_values)
|
|
203
|
+
"#{name}#{labels_block(pairs)} #{value}\n"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def default_label_keys(n)
|
|
207
|
+
Array.new(n) { |i| "label_#{i}" }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def labels_block(pairs)
|
|
211
|
+
return '' if pairs.empty?
|
|
212
|
+
|
|
213
|
+
inside = pairs.map { |k, v| %(#{k}="#{escape_label(v.to_s)}") }.join(',')
|
|
214
|
+
"{#{inside}}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def escape_label(value)
|
|
218
|
+
out = +''
|
|
219
|
+
value.each_char do |c|
|
|
220
|
+
out << case c
|
|
221
|
+
when '\\' then '\\\\'
|
|
222
|
+
when '"' then '\\"'
|
|
223
|
+
when "\n" then '\\n'
|
|
224
|
+
else
|
|
225
|
+
c
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
out
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Render histogram bucket edges with a stable representation. Integer-
|
|
232
|
+
# valued floats stay as `2.5` (not `2.5000000000000004`); fractional
|
|
233
|
+
# ones round to 6 places, plenty for scrape stability.
|
|
234
|
+
def format_float(v)
|
|
235
|
+
f = v.to_f
|
|
236
|
+
f == f.to_i ? f.to_i.to_s : format('%.6g', f)
|
|
237
|
+
end
|
|
238
|
+
|
|
89
239
|
def append_metric(buf, name, help, type, value)
|
|
90
240
|
buf << "# HELP #{name} #{help}\n"
|
|
91
241
|
buf << "# TYPE #{name} #{type}\n"
|
data/lib/hyperion/request.rb
CHANGED
|
@@ -18,7 +18,20 @@ module Hyperion
|
|
|
18
18
|
freeze
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Case-insensitive header lookup. Phase 11 — Hyperion's parser stores
|
|
22
|
+
# header names lowercased (the parser's normalisation contract), and
|
|
23
|
+
# the in-tree hot-path callers (Adapter::Rack#build_env,
|
|
24
|
+
# Connection#should_keep_alive?, Handshake#validate) all pass frozen
|
|
25
|
+
# lowercase literals. Pre-Phase-11 the unconditional `name.downcase`
|
|
26
|
+
# allocated a redundant copy per call. Fast-path direct hash lookup;
|
|
27
|
+
# only fall through to `downcase` when the literal lookup misses,
|
|
28
|
+
# which preserves the case-insensitive contract for mixed-case callers
|
|
29
|
+
# (specs, third-party middleware) without paying the allocation on
|
|
30
|
+
# every request.
|
|
21
31
|
def header(name)
|
|
32
|
+
v = @headers[name]
|
|
33
|
+
return v unless v.nil?
|
|
34
|
+
|
|
22
35
|
@headers[name.downcase]
|
|
23
36
|
end
|
|
24
37
|
end
|