Skip to content
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

gradually add metrics capabilities to Instrumentation::Base #1324

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions instrumentation/base/Appraisals
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

appraise 'base' do
remove_gem 'opentelemetry-metrics-api'
remove_gem 'opentelemetry-metrics-sdk'
end

appraise 'metrics-api' do
remove_gem 'opentelemetry-metrics-sdk'
end

appraise 'metrics-sdk' do # rubocop: disable Lint/EmptyBlock
end
3 changes: 3 additions & 0 deletions instrumentation/base/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@

source 'https://rubygems.org'

gem 'opentelemetry-metrics-api', '~> 0.2'
gem 'opentelemetry-metrics-sdk'

gemspec
34 changes: 25 additions & 9 deletions instrumentation/base/lib/opentelemetry/instrumentation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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?
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
Expand Down
192 changes: 192 additions & 0 deletions instrumentation/base/lib/opentelemetry/instrumentation/metrics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

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
# (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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'opentelemetry-common', '~> 0.21'
spec.add_dependency 'opentelemetry-registry', '~> 0.1'

spec.add_development_dependency 'appraisal', '~> 2.5'
spec.add_development_dependency 'bundler', '~> 2.4'
spec.add_development_dependency 'minitest', '~> 5.0'
spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3'
Expand Down
Loading