-
Notifications
You must be signed in to change notification settings - Fork 200
feat: gradually add metrics capabilities to Instrumentation::Base #1324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# frozen_string_literal: true | ||
|
||
appraise 'base' do # rubocop: disable Lint/EmptyBlock | ||
end | ||
|
||
appraise 'metrics-api' do | ||
gem 'opentelemetry-metrics-api' | ||
end | ||
|
||
appraise 'metrics-sdk' do | ||
gem 'opentelemetry-sdk' | ||
gem 'opentelemetry-metrics-sdk' | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,8 +69,9 @@ class << self | |
integer: ->(v) { v.is_a?(Integer) }, | ||
string: ->(v) { v.is_a?(String) } | ||
}.freeze | ||
SINGLETON_MUTEX = Thread::Mutex.new | ||
|
||
private_constant :NAME_REGEX, :VALIDATORS | ||
private_constant :NAME_REGEX, :VALIDATORS, :SINGLETON_MUTEX | ||
|
||
private :new | ||
|
||
|
@@ -163,8 +164,10 @@ def option(name, default:, validate:) | |
end | ||
|
||
def instance | ||
@instance ||= new(instrumentation_name, instrumentation_version, install_blk, | ||
present_blk, compatible_blk, options) | ||
@instance || SINGLETON_MUTEX.synchronize do | ||
@instance ||= new(instrumentation_name, instrumentation_version, install_blk, | ||
present_blk, compatible_blk, options) | ||
end | ||
end | ||
|
||
private | ||
|
@@ -189,13 +192,15 @@ def infer_version | |
end | ||
end | ||
|
||
attr_reader :name, :version, :config, :installed, :tracer | ||
attr_reader :name, :version, :config, :installed, :tracer, :meter | ||
|
||
alias installed? installed | ||
|
||
require_relative 'metrics' | ||
prepend(OpenTelemetry::Instrumentation::Metrics) | ||
|
||
# rubocop:disable Metrics/ParameterLists | ||
def initialize(name, version, install_blk, present_blk, | ||
compatible_blk, options) | ||
def initialize(name, version, install_blk, present_blk, compatible_blk, options) | ||
@name = name | ||
@version = version | ||
@install_blk = install_blk | ||
|
@@ -204,7 +209,8 @@ def initialize(name, version, install_blk, present_blk, | |
@config = {} | ||
@installed = false | ||
@options = options | ||
@tracer = OpenTelemetry::Trace::Tracer.new | ||
@tracer = OpenTelemetry::Trace::Tracer.new # default no-op tracer | ||
@meter = OpenTelemetry::Metrics::Meter.new if defined?(OpenTelemetry::Metrics::Meter) # default no-op meter | ||
end | ||
# rubocop:enable Metrics/ParameterLists | ||
|
||
|
@@ -217,10 +223,12 @@ def install(config = {}) | |
return true if installed? | ||
|
||
@config = config_options(config) | ||
|
||
return false unless installable?(config) | ||
|
||
prepare_install | ||
instance_exec(@config, &@install_blk) | ||
@tracer = OpenTelemetry.tracer_provider.tracer(name, version) | ||
|
||
@installed = true | ||
end | ||
|
||
|
@@ -263,6 +271,10 @@ def enabled?(config = nil) | |
|
||
private | ||
|
||
def prepare_install | ||
@tracer = OpenTelemetry.tracer_provider.tracer(name, version) | ||
end | ||
|
||
# The config_options method is responsible for validating that the user supplied | ||
# config hash is valid. | ||
# Unknown configuration keys are not included in the final config hash. | ||
|
@@ -317,13 +329,17 @@ def config_options(user_config) | |
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable | ||
# the instrumentation, all other values will enable it. | ||
def enabled_by_env_var? | ||
!disabled_by_env_var? | ||
end | ||
|
||
def disabled_by_env_var? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't actually see where this is being used. |
||
var_name = name.dup.tap do |n| | ||
n.upcase! | ||
n.gsub!('::', '_') | ||
n.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_') | ||
n << '_ENABLED' | ||
end | ||
ENV[var_name] != 'false' | ||
ENV[var_name] == 'false' | ||
end | ||
|
||
# Checks to see if the user has passed any environment variables that set options | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
# frozen_string_literal: true | ||
|
||
# Copyright The OpenTelemetry Authors | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
begin | ||
# mitigate potential load order issues by always requiring | ||
# the metrics-api gem if it is in the bundle. This module is | ||
# intended to function with or without it, but loading it after | ||
# any of the definition queries have been made may cause false negatives. | ||
require 'opentelemetry-metrics-api' | ||
rescue LoadError # rubocop: disable Lint/SuppressedException | ||
end | ||
|
||
module OpenTelemetry | ||
module Instrumentation | ||
# Extensions to Instrumentation::Base that handle metrics instruments. | ||
# The goal here is to allow metrics to be added gradually to instrumentation libraries, | ||
# without requiring that the metrics-sdk or metrics-api gems are present in the bundle | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Instrumentation base has a dependency on the opentelemetry-api gem, if we want to start adding metric support in the instrumentation libraries the way forward is to get the metrics api stable and merged in the api proper. |
||
# (if they are not, or if the metrics-api gem does not meet the minimum version requirement, | ||
# the no-op edition is installed.) | ||
module Metrics | ||
METER_TYPES = %i[ | ||
counter | ||
observable_counter | ||
histogram | ||
gauge | ||
observable_gauge | ||
up_down_counter | ||
observable_up_down_counter | ||
].freeze | ||
|
||
def self.prepended(base) | ||
base.prepend(Compatibility) | ||
base.extend(Compatibility) | ||
base.extend(Registration) | ||
|
||
if base.metrics_compatible? | ||
base.prepend(Extensions) | ||
else | ||
base.prepend(NoopExtensions) | ||
end | ||
end | ||
|
||
# Methods to check whether the metrics API is defined | ||
# and is a compatible version | ||
module Compatibility | ||
METRICS_API_MINIMUM_GEM_VERSION = Gem::Version.new('0.2.0') | ||
|
||
def metrics_defined? | ||
defined?(OpenTelemetry::Metrics) | ||
end | ||
|
||
def metrics_compatible? | ||
metrics_defined? && Gem.loaded_specs['opentelemetry-metrics-api'].version >= METRICS_API_MINIMUM_GEM_VERSION | ||
end | ||
|
||
extend(self) | ||
end | ||
|
||
# class-level methods to declare and register metrics instruments. | ||
# This can be extended even if metrics is not active or present. | ||
module Registration | ||
METER_TYPES.each do |instrument_kind| | ||
define_method(instrument_kind) do |name, **opts, &block| | ||
opts[:callback] ||= block if block | ||
register_instrument(instrument_kind, name, **opts) | ||
end | ||
end | ||
|
||
def register_instrument(kind, name, **opts) | ||
key = [kind, name] | ||
if instrument_configs.key?(key) | ||
warn("Duplicate instrument configured for #{self}: #{key.inspect}") | ||
else | ||
instrument_configs[key] = opts | ||
end | ||
end | ||
|
||
def instrument_configs | ||
@instrument_configs ||= {} | ||
end | ||
end | ||
|
||
# No-op instance methods for metrics instruments. | ||
module NoopExtensions | ||
METER_TYPES.each do |kind| | ||
define_method(kind) { |*, **| } # rubocop: disable Lint/EmptyBlock | ||
end | ||
|
||
def with_meter; end | ||
|
||
def metrics_enabled? | ||
false | ||
end | ||
end | ||
|
||
# Instance methods for metrics instruments. | ||
module Extensions | ||
%i[ | ||
counter | ||
observable_counter | ||
histogram | ||
gauge | ||
observable_gauge | ||
up_down_counter | ||
observable_up_down_counter | ||
].each do |kind| | ||
define_method(kind) do |name| | ||
get_metrics_instrument(kind, name) | ||
end | ||
end | ||
|
||
# This is based on a variety of factors, and should be invalidated when @config changes. | ||
# It should be explicitly set in `prepare_install` for now. | ||
def metrics_enabled? | ||
!!@metrics_enabled | ||
end | ||
|
||
# @api private | ||
# ONLY yields if the meter is enabled. | ||
def with_meter | ||
yield @meter if metrics_enabled? | ||
end | ||
|
||
private | ||
|
||
def compute_metrics_enabled | ||
return false unless metrics_compatible? | ||
return false if metrics_disabled_by_env_var? | ||
|
||
!!@config[:metrics] || metrics_enabled_by_env_var? | ||
end | ||
|
||
# Checks if this instrumentation's metrics are enabled by env var. | ||
# This follows the conventions as outlined above, using `_METRICS_ENABLED` as a suffix. | ||
# Unlike INSTRUMENTATION_*_ENABLED variables, these are explicitly opt-in (i.e. | ||
# if the variable is unset, and `metrics: true` is not in the instrumentation's config, | ||
# the metrics will not be enabled) | ||
def metrics_enabled_by_env_var? | ||
ENV.key?(metrics_env_var_name) && ENV[metrics_env_var_name] != 'false' | ||
end | ||
|
||
def metrics_disabled_by_env_var? | ||
ENV[metrics_env_var_name] == 'false' | ||
end | ||
|
||
def metrics_env_var_name | ||
@metrics_env_var_name ||= | ||
begin | ||
var_name = name.dup | ||
var_name.upcase! | ||
var_name.gsub!('::', '_') | ||
var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_') | ||
var_name << '_METRICS_ENABLED' | ||
var_name | ||
end | ||
end | ||
|
||
def prepare_install | ||
@metrics_enabled = compute_metrics_enabled | ||
if metrics_defined? | ||
@metrics_instruments = {} | ||
@instrument_mutex = Mutex.new | ||
end | ||
|
||
@meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled? | ||
|
||
super | ||
end | ||
|
||
def get_metrics_instrument(kind, name) | ||
# TODO: we should probably return *something* | ||
# if metrics is not enabled, but if the api is undefined, | ||
# it's unclear exactly what would be suitable. | ||
# For now, there are no public methods that call this | ||
# if metrics isn't defined. | ||
return unless metrics_defined? | ||
|
||
@metrics_instruments.fetch([kind, name]) do |key| | ||
@instrument_mutex.synchronize do | ||
@metrics_instruments[key] ||= create_configured_instrument(kind, name) | ||
end | ||
end | ||
end | ||
|
||
def create_configured_instrument(kind, name) | ||
config = self.class.instrument_configs[[kind, name]] | ||
|
||
if config.nil? | ||
Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'") | ||
return | ||
end | ||
|
||
meter.public_send(:"create_#{kind}", name, **config) | ||
end | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the rational for why this mutex lock is necessary?