From 7fa9f499e164741ee62511176b30c4ebb84984f9 Mon Sep 17 00:00:00 2001 From: cn Date: Fri, 13 Nov 2026 17:27:49 +0100 Subject: [PATCH] gems: include vendored metrisk without hitime dependency hitime is only needed for Metriks::Timer and requires native extensions. Those do not compile with default settings under Ubuntu 26.04 or Alpine 3.24. --- .rubocop.yml | 6 ++ dyndnsd.gemspec | 1 - lib/dyndnsd/textfile_reporter.rb | 28 ------- lib/metriks.rb | 27 ++++++ lib/metriks/atomic.rb | 34 ++++++++ lib/metriks/ewma.rb | 66 +++++++++++++++ lib/metriks/meter.rb | 88 ++++++++++++++++++++ lib/metriks/registry.rb | 127 +++++++++++++++++++++++++++++ lib/metriks/reporter/graphite.rb | 91 +++++++++++++++++++++ lib/metriks/reporter/proc_title.rb | 63 ++++++++++++++ 10 files changed, 502 insertions(+), 29 deletions(-) create mode 100644 lib/metriks.rb create mode 100644 lib/metriks/atomic.rb create mode 100644 lib/metriks/ewma.rb create mode 100644 lib/metriks/meter.rb create mode 100644 lib/metriks/registry.rb create mode 100644 lib/metriks/reporter/graphite.rb create mode 100644 lib/metriks/reporter/proc_title.rb diff --git a/.rubocop.yml b/.rubocop.yml index ff4533e..0e3b6c5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,9 +2,15 @@ plugins: - rubocop-rake - rubocop-rspec +inherit_mode: + merge: [Exclude] + AllCops: TargetRubyVersion: '3.0' NewCops: enable + Exclude: + - 'lib/metriks/**/*' + - 'lib/metriks.rb' Gemspec/DevelopmentDependencies: EnforcedStyle: gemspec diff --git a/dyndnsd.gemspec b/dyndnsd.gemspec index d8bc0f7..b93906a 100644 --- a/dyndnsd.gemspec +++ b/dyndnsd.gemspec @@ -31,7 +31,6 @@ Gem::Specification.new do |s| s.add_dependency 'async-dns', '~> 1.3.0' s.add_dependency 'base64', '~> 0.3.0' # needed for async s.add_dependency 'logger', '>= 1.6', '< 1.8' - s.add_dependency 'metriks' s.add_dependency 'opentelemetry-exporter-jaeger', '~> 0.22.0' s.add_dependency 'opentelemetry-instrumentation-rack', '~> 0.22.0' s.add_dependency 'opentelemetry-sdk', '~> 1.2.0' diff --git a/lib/dyndnsd/textfile_reporter.rb b/lib/dyndnsd/textfile_reporter.rb index 91896cf..c3033db 100644 --- a/lib/dyndnsd/textfile_reporter.rb +++ b/lib/dyndnsd/textfile_reporter.rb @@ -58,34 +58,6 @@ module Dyndnsd :count, :one_minute_rate, :five_minute_rate, :fifteen_minute_rate, :mean_rate ] - when Metriks::Counter - write_metric f, name, metric, [ - :count - ] - when Metriks::UtilizationTimer - write_metric f, name, metric, [ - :count, :one_minute_rate, :five_minute_rate, - :fifteen_minute_rate, :mean_rate, - :min, :max, :mean, :stddev, - :one_minute_utilization, :five_minute_utilization, - :fifteen_minute_utilization, :mean_utilization - ], [ - :median, :get_95th_percentile - ] - when Metriks::Timer - write_metric f, name, metric, [ - :count, :one_minute_rate, :five_minute_rate, - :fifteen_minute_rate, :mean_rate, - :min, :max, :mean, :stddev - ], [ - :median, :get_95th_percentile - ] - when Metriks::Histogram - write_metric f, name, metric, [ - :count, :min, :max, :mean, :stddev - ], [ - :median, :get_95th_percentile - ] end end end diff --git a/lib/metriks.rb b/lib/metriks.rb new file mode 100644 index 0000000..1664ad6 --- /dev/null +++ b/lib/metriks.rb @@ -0,0 +1,27 @@ +# Vendored from metriks 0.9.9.8 — https://github.com/eric/metriks +# Copyright (c) 2012 Eric Lindvall, MIT licensed. +# Trimmed to the subset dyndnsd uses (meters + registry + reporters); +# counter/gauge/timer/utilization_timer/histogram support was removed, +# and the 'atomic'/'avl_tree'/'hitimes' dependencies were replaced or dropped. + +module Metriks + VERSION = '0.9.9.8' + + def self.get(name) + Registry.default.get(name) + end + + def self.meter(name) + Registry.default.meter(name) + end + + def self.registry + Registry.default + end +end + +require 'metriks/atomic' +require 'metriks/ewma' +require 'metriks/meter' +require 'metriks/registry' +require 'metriks/reporter/proc_title' diff --git a/lib/metriks/atomic.rb b/lib/metriks/atomic.rb new file mode 100644 index 0000000..f54857f --- /dev/null +++ b/lib/metriks/atomic.rb @@ -0,0 +1,34 @@ +# Minimal pure-Ruby replacement for the 'atomic' gem, covering the exact API +# the vendored metriks code uses (Atomic.new, #value, #value=, #update, +# #compare_and_swap). Mutex-guarded instead of relying on a native extension. +module Metriks + class Atomic + def initialize(value = nil) + @value = value + @mutex = Mutex.new + end + + def value + @mutex.synchronize { @value } + end + + def value=(new_value) + @mutex.synchronize { @value = new_value } + end + + def update + @mutex.synchronize { @value = yield(@value) } + end + + def compare_and_swap(expected, new_value) + @mutex.synchronize do + if @value == expected + @value = new_value + true + else + false + end + end + end + end +end diff --git a/lib/metriks/ewma.rb b/lib/metriks/ewma.rb new file mode 100644 index 0000000..f9490fe --- /dev/null +++ b/lib/metriks/ewma.rb @@ -0,0 +1,66 @@ +# Vendored from metriks 0.9.9.8 — https://github.com/eric/metriks +# Copyright (c) 2012 Eric Lindvall, MIT licensed. +# Trimmed to the subset dyndnsd uses (meters + registry + reporters). + + +module Metriks + class EWMA + INTERVAL = 5.0 + SECONDS_PER_MINUTE = 60.0 + + ONE_MINUTE = 1 + FIVE_MINUTES = 5 + FIFTEEN_MINUTES = 15 + + M1_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE) + M5_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIVE_MINUTES) + M15_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIFTEEN_MINUTES) + + def self.new_m1 + new(M1_ALPHA, INTERVAL) + end + + def self.new_m5 + new(M5_ALPHA, INTERVAL) + end + + def self.new_m15 + new(M15_ALPHA, INTERVAL) + end + + def initialize(alpha, interval) + @alpha = alpha + @interval = interval + + @initialized = false + @rate = Atomic.new(0.0) + @uncounted = Atomic.new(0) + end + + def clear + @initialized = false + @rate.value = 0.0 + @uncounted.value = 0 + end + + def update(value) + @uncounted.update { |v| v + value } + end + + def tick + count = @uncounted.swap(0) + instant_rate = count / @interval.to_f + + if @initialized + @rate.update { |v| v + @alpha * (instant_rate - v) } + else + @rate.value = instant_rate + @initialized = true + end + end + + def rate + @rate.value + end + end +end \ No newline at end of file diff --git a/lib/metriks/meter.rb b/lib/metriks/meter.rb new file mode 100644 index 0000000..020d15a --- /dev/null +++ b/lib/metriks/meter.rb @@ -0,0 +1,88 @@ +# Vendored from metriks 0.9.9.8 — https://github.com/eric/metriks +# Copyright (c) 2012 Eric Lindvall, MIT licensed. +# Trimmed to the subset dyndnsd uses (meters + registry + reporters). + + +require 'metriks/ewma' + +module Metriks + class Meter + TICK_INTERVAL = 5.0 + + def initialize(averager_klass = Metriks::EWMA) + @count = Atomic.new(0) + @start_time = Time.now.to_f + @last_tick = Atomic.new(@start_time) + + @m1_rate = averager_klass.new_m1 + @m5_rate = averager_klass.new_m5 + @m15_rate = averager_klass.new_m15 + end + + def clear + @count.value = 0 + @start_time = Time.now.to_f + @last_tick.value = @start_time + @m1_rate.clear + @m5_rate.clear + @m15_rate.clear + end + + def tick + @m1_rate.tick + @m5_rate.tick + @m15_rate.tick + end + + def tick_if_nessesary + old_tick = @last_tick.value + new_tick = Time.new.to_f + age = new_tick - old_tick + if age > TICK_INTERVAL && @last_tick.compare_and_swap(old_tick, new_tick) + required_ticks = age / TICK_INTERVAL + required_ticks.to_i.times do + tick + end + end + end + + def mark(val = 1) + tick_if_nessesary + @count.update { |v| v + val } + @m1_rate.update(val) + @m5_rate.update(val) + @m15_rate.update(val) + end + + def count + @count.value + end + + def one_minute_rate + tick_if_nessesary + @m1_rate.rate + end + + def five_minute_rate + tick_if_nessesary + @m5_rate.rate + end + + def fifteen_minute_rate + tick_if_nessesary + @m15_rate.rate + end + + def mean_rate + if count == 0 + return 0.0 + else + elapsed = Time.now.to_f - @start_time + count / elapsed + end + end + + def stop + end + end +end \ No newline at end of file diff --git a/lib/metriks/registry.rb b/lib/metriks/registry.rb new file mode 100644 index 0000000..dd745f8 --- /dev/null +++ b/lib/metriks/registry.rb @@ -0,0 +1,127 @@ +# Vendored from metriks 0.9.9.8 — https://github.com/eric/metriks +# Copyright (c) 2012 Eric Lindvall, MIT licensed. +# Trimmed to the subset dyndnsd uses (meters + registry + reporters). + +require 'metriks/meter' + +module Metriks + # Public: A collection of metrics + class Registry + # Public: The default registry for the process. + # + # Returns the default Registry for the process. + def self.default + @default ||= new + end + + # Public: Initializes a new Registry. + def initialize + @mutex = Mutex.new + @metrics = {} + end + + # Public: Clear all of the metrics in the Registry. This ensures all + # metrics that have been added are stopped. + # + # Returns nothing. + def clear + @mutex.synchronize do + @metrics.each do |key, metric| + metric.stop if metric.respond_to?(:stop) + end + + @metrics = {} + end + end + + # Public: Clear all of the metrics in the Registry. This has the same + # effect as calling #clear. + # + # Returns nothing. + def stop + clear + end + + # Public: Iterate over all of the counters. + # + # Examples + # + # registry.each do |name, metric| + # puts name + # end + # + # Returns nothing. + def each(&block) + metrics = @mutex.synchronize do + @metrics.dup + end + + metrics.each(&block) + end + + # Public: Fetch or create a new meter metric. Meters are a counter that + # tracks throughput along with the count. + # + # name - The String name of the metric to define or fetch + # + # Examples + # + # registry.meter('resque.calls') + # + # Returns the Metriks::Meter identified by the name. + def meter(name) + add_or_get(name, Metriks::Meter) + end + + # Public: Fetch an existing metric. + # + # name - The String name of the metric to fetch + # + # Examples + # + # registry.get('rack.utilization') + # + # Returns the metric or nil. + def get(name) + @mutex.synchronize do + @metrics[name] + end + end + + # Public: Add a new metric. + # + # name - The String name of the metric to add + # metric - The metric instance to add + # + # Examples + # + # registry.add('method.calls', Metriks::Counter.new) + # + # Returns nothing. + # Raises RuntimeError if the metric name is already defined + def add(name, metric) + @mutex.synchronize do + if @metrics[name] + raise "Metric '#{name}' already defined" + else + @metrics[name] = metric + end + end + end + + protected + def add_or_get(name, klass, &create_metric) + @mutex.synchronize do + if metric = @metrics[name] + if !metric.is_a?(klass) + raise "Metric already defined as '#{metric.class}'" + else + return metric + end + else + @metrics[name] = create_metric ? create_metric.call : klass.new + end + end + end + end +end diff --git a/lib/metriks/reporter/graphite.rb b/lib/metriks/reporter/graphite.rb new file mode 100644 index 0000000..d67947b --- /dev/null +++ b/lib/metriks/reporter/graphite.rb @@ -0,0 +1,91 @@ +# Vendored from metriks 0.9.9.8 — https://github.com/eric/metriks +# Copyright (c) 2012 Eric Lindvall, MIT licensed. +# Trimmed to the subset dyndnsd uses (meters + registry + reporters). + +require 'socket' + +module Metriks::Reporter + class Graphite + attr_reader :host, :port + + def initialize(host, port, options = {}) + @host = host + @port = port + + @prefix = options[:prefix] + + @registry = options[:registry] || Metriks::Registry.default + @interval = options[:interval] || 60 + @on_error = options[:on_error] || proc { |ex| } + end + + def socket + @socket = nil if @socket && @socket.closed? + @socket ||= TCPSocket.new(@host, @port) + end + + def start + @thread ||= Thread.new do + loop do + sleep @interval + + Thread.new do + begin + write + rescue Exception => ex + @on_error[ex] rescue nil + end + end + end + end + end + + def stop + @thread.kill if @thread + @thread = nil + end + + def restart + stop + start + end + + def write + @registry.each do |name, metric| + case metric + when Metriks::Meter + write_metric name, metric, [ + :count, :one_minute_rate, :five_minute_rate, + :fifteen_minute_rate, :mean_rate + ] + end + end + end + + def write_metric(base_name, metric, keys, snapshot_keys = []) + time = Time.now.to_i + + base_name = base_name.to_s.gsub(/ +/, '_') + if @prefix + base_name = "#{@prefix}.#{base_name}" + end + + keys.flatten.each do |key| + name = key.to_s.gsub(/^get_/, '') + value = metric.send(key) + socket.write("#{base_name}.#{name} #{value} #{time}\n") + end + + unless snapshot_keys.empty? + snapshot = metric.snapshot + snapshot_keys.flatten.each do |key| + name = key.to_s.gsub(/^get_/, '') + value = snapshot.send(key) + socket.write("#{base_name}.#{name} #{value} #{time}\n") + end + end + rescue Errno::EPIPE + socket.close + end + end +end diff --git a/lib/metriks/reporter/proc_title.rb b/lib/metriks/reporter/proc_title.rb new file mode 100644 index 0000000..85f05e2 --- /dev/null +++ b/lib/metriks/reporter/proc_title.rb @@ -0,0 +1,63 @@ +# Vendored from metriks 0.9.9.8 — https://github.com/eric/metriks +# Copyright (c) 2012 Eric Lindvall, MIT licensed. +# Trimmed to the subset dyndnsd uses (meters + registry + reporters). + +module Metriks::Reporter + class ProcTitle + def initialize(options = {}) + @rounding = options[:rounding] || 1 + @prefix = options[:prefix] || $0.dup + + @interval = options[:interval] || 5 + @on_error = options[:on_error] || proc { |ex| } + + @metrics = [] + end + + def add(name, suffix = nil, &block) + @metrics << [ name, suffix, block ] + end + + def empty? + @metrics.empty? + end + + def start + @thread ||= Thread.new do + loop do + begin + unless @metrics.empty? + title = generate_title + if title && !title.empty? + $0 = "#{@prefix} #{title}" + end + end + rescue Exception => ex + @on_error[ex] rescue nil + end + sleep @interval + end + end + end + + def stop + @thread.kill if @thread + @thread = nil + end + + def restart + stop + start + end + + protected + def generate_title + @metrics.collect do |name, suffix, block| + val = block.call + val = "%.#{@rounding}f" % val if val.is_a?(Float) + + "#{name}: #{val}#{suffix}" + end.join(' ') + end + end +end \ No newline at end of file