This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "metrics/version"
|
||||
require_relative "metrics/provider"
|
||||
require_relative "metrics/tags"
|
||||
|
||||
# @namespace
|
||||
module Metrics
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "config"
|
||||
|
||||
module Metrics
|
||||
module Backend
|
||||
end
|
||||
|
||||
Config::DEFAULT.require_backend
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2023-2024, by Samuel Williams.
|
||||
|
||||
require_relative "../metric"
|
||||
|
||||
module Metrics
|
||||
module Backend
|
||||
module Capture
|
||||
class Metric < Metrics::Metric
|
||||
def initialize(...)
|
||||
super
|
||||
|
||||
@values = []
|
||||
@tags = Set.new
|
||||
@sample_rates = []
|
||||
end
|
||||
|
||||
attr :values
|
||||
attr :tags
|
||||
attr :sample_rates
|
||||
|
||||
def emit(value, tags: nil, sample_rate: 1.0)
|
||||
@values << value
|
||||
@tags.merge(tags) if tags
|
||||
@sample_rates << sample_rate
|
||||
end
|
||||
|
||||
def as_json
|
||||
{
|
||||
name: @name,
|
||||
type: @type,
|
||||
description: @description,
|
||||
unit: @unit,
|
||||
values: @values,
|
||||
tags: @tags.to_a.sort,
|
||||
sample_rates: @sample_rates.sort.uniq
|
||||
}
|
||||
end
|
||||
|
||||
def to_json(...)
|
||||
as_json.to_json(...)
|
||||
end
|
||||
end
|
||||
|
||||
def self.metrics
|
||||
@metrics ||= []
|
||||
end
|
||||
|
||||
module Interface
|
||||
def metric(name, type, description: nil, unit: nil, &block)
|
||||
metric = Metric.new(name, type, description, unit)
|
||||
|
||||
Capture.metrics << metric
|
||||
|
||||
return metric
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Interface = Capture::Interface
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "../metric"
|
||||
|
||||
require "console"
|
||||
|
||||
module Metrics
|
||||
module Backend
|
||||
module Console
|
||||
class Metric < Metrics::Metric
|
||||
def emit(value, tags: nil, sample_rate: 1.0)
|
||||
::Console.logger.info(self, @name, value, tags)
|
||||
end
|
||||
end
|
||||
|
||||
module Interface
|
||||
def metric(name, type, description: nil, unit: nil, &block)
|
||||
return Metric.new(name, type, description, unit)
|
||||
end
|
||||
|
||||
# def metric_call_counter(name, description: nil, tags: nil)
|
||||
# metric = self.metric(...)
|
||||
#
|
||||
# self.define_method(name) do
|
||||
# metric.emit(1)
|
||||
# super
|
||||
# end
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
Interface = Console::Interface
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "../metric"
|
||||
|
||||
module Metrics
|
||||
module Backend
|
||||
module Test
|
||||
VALID_METRIC_NAME = /\A[a-z0-9\-_\.]{1,128}\Z/i
|
||||
VALID_TAG = /\A[a-z][a-z0-9\-_\.:\\]{0,127}\Z/i
|
||||
|
||||
class Metric < Metrics::Metric
|
||||
def emit(value, tags: nil, sample_rate: 1.0)
|
||||
unless value.is_a?(Numeric)
|
||||
raise ArgumentError, "Value must be numeric!"
|
||||
end
|
||||
|
||||
tags&.each do |tag|
|
||||
raise ArgumentError, "Invalid tag (must be String): #{tag.inspect}!" unless tag.is_a?(String)
|
||||
|
||||
# We should let the underlying backend handle any tag limitations, e.g. converting invalid characters to underscores, etc.
|
||||
#
|
||||
# unless tag =~ VALID_TAG
|
||||
# raise ArgumentError, "Invalid tag (must match #{VALID_TAG}): #{tag.inspect}!"
|
||||
# end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Interface
|
||||
def metric(name, type, description: nil, unit: nil, &block)
|
||||
unless name.is_a?(String)
|
||||
raise ArgumentError, "Invalid name (must be String): #{name.inspect}!"
|
||||
end
|
||||
|
||||
unless name =~ VALID_METRIC_NAME
|
||||
raise ArgumentError, "Invalid name (must match #{VALID_METRIC_NAME}): #{name.inspect}!"
|
||||
end
|
||||
|
||||
unless type.is_a?(Symbol)
|
||||
raise ArgumentError, "Invalid type (must be Symbol): #{type.inspect}!"
|
||||
end
|
||||
|
||||
# Description is optional but must be string if given:
|
||||
if description
|
||||
unless description.is_a?(String)
|
||||
raise ArgumentError, "Invalid description (must be String): #{description.inspect}!"
|
||||
end
|
||||
end
|
||||
|
||||
# Unit is optional but must be string if given:
|
||||
if unit
|
||||
unless unit.is_a?(String)
|
||||
raise ArgumentError, "Invalid unit (must be String): #{unit.inspect}!"
|
||||
end
|
||||
end
|
||||
|
||||
return Metric.new(name, type, description, unit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Interface = Test::Interface
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
module Metrics
|
||||
# Represents a configuration for the metrics library.
|
||||
class Config
|
||||
DEFAULT_PATH = ENV.fetch("METRICS_CONFIG_DEFAULT_PATH", "config/metrics.rb")
|
||||
|
||||
# Load the configuration from the given path.
|
||||
# @parameter path [String] The path to the configuration file.
|
||||
# @returns [Config] The loaded configuration.
|
||||
def self.load(path)
|
||||
config = self.new
|
||||
|
||||
if File.exist?(path)
|
||||
config.instance_eval(File.read(path), path)
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
# Load the default configuration.
|
||||
# @returns [Config] The default configuration.
|
||||
def self.default
|
||||
@default ||= self.load(DEFAULT_PATH)
|
||||
end
|
||||
|
||||
# Prepare the backend, e.g. by loading additional libraries or instrumentation.
|
||||
def prepare
|
||||
end
|
||||
|
||||
# Require a specific metrics backend implementation.
|
||||
def require_backend(env = ENV)
|
||||
if backend = env["METRICS_BACKEND"]
|
||||
begin
|
||||
if require(backend)
|
||||
Metrics.extend(Backend::Interface)
|
||||
|
||||
return true
|
||||
end
|
||||
rescue LoadError => error
|
||||
warn "Unable to load metrics backend: #{backend.inspect}!"
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
# Load the default configuration.
|
||||
DEFAULT = self.default
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2022, by Samuel Williams.
|
||||
|
||||
module Metrics
|
||||
class Metric
|
||||
def initialize(name, type, description, unit)
|
||||
@name = name
|
||||
@type = type
|
||||
@description = description
|
||||
@unit = unit
|
||||
end
|
||||
|
||||
attr :name
|
||||
attr :type
|
||||
attr :description
|
||||
attr :unit
|
||||
|
||||
def emit(value, tags: nil, sample_rate: 1.0)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "backend"
|
||||
|
||||
module Metrics
|
||||
# @returns [Boolean] Whether there is an active backend.
|
||||
def self.enabled?
|
||||
Backend.const_defined?(:Interface)
|
||||
end
|
||||
|
||||
module Provider
|
||||
end
|
||||
|
||||
# A module which contains tracing specific wrappers.
|
||||
module Singleton
|
||||
def metrics_provider
|
||||
@metrics_provider ||= Module.new
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Singleton
|
||||
|
||||
# Bail out if there is no backend configured.
|
||||
if self.enabled?
|
||||
# Extend the specified class in order to emit traces.
|
||||
def self.Provider(klass, &block)
|
||||
klass.extend(Singleton)
|
||||
provider = klass.metrics_provider
|
||||
klass.prepend(provider)
|
||||
|
||||
provider.module_exec(&block) if block_given?
|
||||
|
||||
return provider
|
||||
end
|
||||
|
||||
Config::DEFAULT.prepare
|
||||
else
|
||||
def self.Provider(klass, &block)
|
||||
# Metrics disabled.
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
module Metrics
|
||||
module Tags
|
||||
def self.normalize(tags)
|
||||
return nil unless tags&.any?
|
||||
|
||||
if tags.is_a?(Hash)
|
||||
tags = tags.map{|key, value| "#{key}:#{value}"}
|
||||
end
|
||||
|
||||
return Array(tags)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
module Metrics
|
||||
VERSION = "0.12.1"
|
||||
end
|
||||
Reference in New Issue
Block a user