This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2023-2024, by Samuel Williams.
|
||||
|
||||
# Enable capturing metrics.
|
||||
def capture
|
||||
ENV["METRICS_BACKEND"] = "metrics/backend/capture"
|
||||
require "metrics"
|
||||
end
|
||||
|
||||
# Generate a list of metrics that have been captured.
|
||||
def list
|
||||
Metrics::Backend::Capture.metrics.sort_by!{|metric| metric.name}
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Metrics
|
||||
|
||||
Capture metrics about code execution in a vendor agnostic way. As the author of many libraries which would benefit from metrics, there are few key priorities: (1) zero overhead if metrics are disabled, minimal overhead if enabled, and (2) a small and opinionated interface with standardised semantics.
|
||||
|
||||
[](https://github.com/socketry/metrics/actions?workflow=Test)
|
||||
|
||||
## Features
|
||||
|
||||
- Zero-overhead if tracing is disabled and minimal overhead if enabled.
|
||||
- Small opinionated interface with standardised semantics.
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [project documentation](https://socketry.github.io/metrics/) for more details.
|
||||
|
||||
- [Getting Started](https://socketry.github.io/metrics/guides/getting-started/index) - This guide explains how to use `metrics` for capturing run-time metrics.
|
||||
|
||||
- [Capture](https://socketry.github.io/metrics/guides/capture/index) - This guide explains how to use `metrics` for exporting metric definitions from your application.
|
||||
|
||||
- [Testing](https://socketry.github.io/metrics/guides/testing/index) - This guide explains how to write assertions in your test suite to validate `metrics` are being emitted correctly.
|
||||
|
||||
## Releases
|
||||
|
||||
Please see the [project releases](https://socketry.github.io/metrics/releases/index) for all releases.
|
||||
|
||||
### v0.12.1
|
||||
|
||||
- [Introduce `Metrics::Config` to Expose `prepare` Hook](https://socketry.github.io/metrics/releases/index#introduce-metrics::config-to-expose-prepare-hook)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to this project.
|
||||
|
||||
1. Fork it.
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`).
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`).
|
||||
4. Push to the branch (`git push origin my-new-feature`).
|
||||
5. Create new Pull Request.
|
||||
|
||||
### Developer Certificate of Origin
|
||||
|
||||
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
||||
|
||||
### Community Guidelines
|
||||
|
||||
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
||||
|
||||
## See Also
|
||||
|
||||
- [metrics-backend-datadog](https://github.com/socketry/metrics-backend-datadog) — A Metrics backend for Datadog.
|
||||
- [traces](https://github.com/socketry/traces) — A code tracing interface which follows a similar pattern.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Releases
|
||||
|
||||
## v0.12.1
|
||||
|
||||
### Introduce `Metrics::Config` to Expose `prepare` Hook
|
||||
|
||||
The `metrics` gem uses aspect-oriented programming to wrap existing methods to emit metrics. However, while there are some reasonable defaults for emitting metrics, it can be useful to customize the behavior and level of detail. To that end, the `metrics` gem now optionally loads a `config/metrics.rb` which includes a `prepare` hook that can be used to load additional providers.
|
||||
|
||||
``` ruby
|
||||
# config/metrics.rb
|
||||
|
||||
def prepare
|
||||
require 'metrics/provider/async'
|
||||
require 'metrics/provider/async/http'
|
||||
end
|
||||
```
|
||||
|
||||
The `prepare` method is called immediately after the metrics backend is loaded. You can require any provider you want in this file, or even add your own custom providers.
|
||||
Reference in New Issue
Block a user