lib/opentelemetry/instrumentation/base.rb in opentelemetry-api-0.14.0 vs lib/opentelemetry/instrumentation/base.rb in opentelemetry-api-0.15.0

- old
+ new

@@ -57,15 +57,23 @@ # by environment variable and local configuration. An instrumentation disabled # by environment variable will take precedence over local config. The # convention for environment variable name is the library name, upcased with # '::' replaced by underscores, OPENTELEMETRY shortened to OTEL_{LANG}, and '_ENABLED' appended. # For example: OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED = false. - class Base + class Base # rubocop:disable Metrics/ClassLength class << self NAME_REGEX = /^(?:(?<namespace>[a-zA-Z0-9_:]+):{2})?(?<classname>[a-zA-Z0-9_]+)$/.freeze - private_constant :NAME_REGEX + VALIDATORS = { + array: ->(v) { v.is_a?(Array) }, + boolean: ->(v) { v == true || v == false }, # rubocop:disable Style/MultipleComparison + callable: ->(v) { v.respond_to?(:call) }, + integer: ->(v) { v.is_a?(Integer) }, + string: ->(v) { v.is_a?(String) } + }.freeze + private_constant :NAME_REGEX, :VALIDATORS + private :new # rubocop:disable Style/AccessModifierDeclarations def inherited(subclass) OpenTelemetry.instrumentation_registry.register(subclass) end @@ -127,18 +135,34 @@ # @param [Callable] blk The compatibility block for this instrumentation def compatible(&blk) @compatible_blk = blk end + # The option method is used to define default configuration options + # for the instrumentation library. It requires a name, default value, + # and a validation callable to be provided. + # @param [String] name The name of the configuration option + # @param default The default value to be used, or to used if validation fails + # @param [Callable, Symbol] validate Accepts a callable or a symbol that matches + # a key in the VALIDATORS hash. The supported keys are, :array, :boolean, + # :callable, :integer, :string. + def option(name, default:, validate:) + validate = VALIDATORS[validate] || validate + raise ArgumentError, "validate must be #{VALIDATORS.keys.join(', ')}, or a callable" unless validate.respond_to?(:call) + + @options ||= [] + @options << { name: name, default: default, validate: validate } + end + def instance @instance ||= new(instrumentation_name, instrumentation_version, install_blk, - present_blk, compatible_blk) + present_blk, compatible_blk, options) end private - attr_reader :install_blk, :present_blk, :compatible_blk + attr_reader :install_blk, :present_blk, :compatible_blk, :options def infer_name @inferred_name ||= if (md = name.match(NAME_REGEX)) # rubocop:disable Naming/MemoizedInstanceVariableName md['namespace'] || md['classname'] end @@ -159,18 +183,19 @@ attr_reader :name, :version, :config, :installed, :tracer alias installed? installed def initialize(name, version, install_blk, present_blk, - compatible_blk) + compatible_blk, options) @name = name @version = version @install_blk = install_blk @present_blk = present_blk @compatible_blk = compatible_blk @config = {} @installed = false + @options = options end # Install instrumentation with the given config. The present? and compatible? # will be run first, and install will return false if either fail. Will # return true if install was completed successfully. @@ -178,11 +203,11 @@ # @param [Hash] config The config for this instrumentation def install(config = {}) return true if installed? return false unless installable?(config) - @config = config unless config.nil? + @config = config_options(config) instance_exec(@config, &@install_blk) @tracer ||= OpenTelemetry.tracer_provider.tracer(name, version) @installed = true end @@ -222,9 +247,46 @@ true end private + + # 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. + # Invalid configuration values are logged, and replaced by the default. + # + # @param [Hash] user_config The user supplied configuration hash + def config_options(user_config) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + @options ||= {} + user_config ||= {} + validated_config = @options.each_with_object({}) do |option, h| + option_name = option[:name] + config_value = user_config[option_name] + + value = if config_value.nil? + option[:default] + elsif option[:validate].call(config_value) + config_value + else + OpenTelemetry.logger.warn( + "Instrumentation #{name} configuration option #{option_name} value=#{config_value} " \ + "failed validation, falling back to default value=#{option[:default]}" + ) + option[:default] + end + + h[option_name] = value + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: "Instrumentation #{name} unexpected configuration error") + h[option_name] = option[:default] + end + + dropped_config_keys = user_config.keys - validated_config.keys + OpenTelemetry.logger.warn("Instrumentation #{name} ignored the following unknown configuration options #{dropped_config_keys}") unless dropped_config_keys.empty? + + validated_config + end # Checks to see if this instrumentation is enabled by env var. By convention, the # environment variable will be the instrumentation name upper cased, with '::' # replaced by underscores, OPENTELEMETRY shortened to OTEL_{LANG} and _ENABLED appended. # For example, the, environment variable name for OpenTelemetry::Instrumentation::Sinatra