Skip to content

Commit

Permalink
feat: add default options config helper + env var config option suppo…
Browse files Browse the repository at this point in the history
…rt (#994)

* feat: add default options config helper and env var config option support, update mysql2 to use new format

Co-authored-by: Robert <robertlaurin@users.noreply.github.com>
  • Loading branch information
ericmustin and robertlaurin committed Nov 18, 2021
1 parent 9428b4e commit 9f760ee
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 9 deletions.
72 changes: 66 additions & 6 deletions instrumentation/base/lib/opentelemetry/instrumentation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,20 @@ def compatible(&blk)
# 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)
validator = VALIDATORS[validate] || validate
raise ArgumentError, "validate must be #{VALIDATORS.keys.join(', ')}, or a callable" unless validator.respond_to?(:call) || validator.respond_to?(:include?)

@options ||= []
@options << { name: name, default: default, validate: validate }

validation_type = if VALIDATORS[validate]
validate
elsif validate.respond_to?(:include?)
:enum
else
:callable
end

@options << { name: name, default: default, validator: validator, validation_type: validation_type }
end

def instance
Expand Down Expand Up @@ -257,16 +266,24 @@ def enabled?(config = nil)
# 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
def config_options(user_config) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@options ||= {}
user_config ||= {}
config_overrides = config_overrides_from_env
validated_config = @options.each_with_object({}) do |option, h|
option_name = option[:name]
config_value = user_config[option_name]
config_override = coerce_env_var(config_overrides[option_name], option[:validation_type]) if config_overrides[option_name]

value = if config_value.nil?
value = if config_value.nil? && config_override.nil?
option[:default]
elsif option[:validate].call(config_value)
elsif option[:validator].respond_to?(:include?) && option[:validator].include?(config_override)
config_override
elsif option[:validator].respond_to?(:include?) && option[:validator].include?(config_value)
config_value
elsif option[:validator].respond_to?(:call) && option[:validator].call(config_override)
config_override
elsif option[:validator].respond_to?(:call) && option[:validator].call(config_value)
config_value
else
OpenTelemetry.logger.warn(
Expand Down Expand Up @@ -303,6 +320,49 @@ def enabled_by_env_var?
end
ENV[var_name] != 'false'
end

def config_overrides_from_env
var_name = name.dup.tap do |n|
n.upcase!
n.gsub!('::', '_')
n.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
n << '_CONFIG_OPTS'
end

environment_config_overrides = {}
env_config_options = ENV[var_name]&.split(';')

return environment_config_overrides if env_config_options.nil?

env_config_options.each_with_object(environment_config_overrides) do |env_config_option, eco|
parts = env_config_option.split('=')
option_name = parts[0].to_sym
eco[option_name] = parts[1]
end

environment_config_overrides
end

def coerce_env_var(env_var, validation_type) # rubocop:disable Metrics/CyclomaticComplexity
case validation_type
when :array
env_var.split(',').map(&:strip)
when :boolean
env_var.to_s.strip.downcase == 'true'
when :integer
env_var.to_i
when :string
env_var.to_s.strip
when :enum
env_var.to_s.strip.to_sym
when :callable
OpenTelemetry.logger.warn(
"Instrumentation #{name} options that accept a callable are not " \
"configurable using environment variables. Ignoring raw value: #{env_var}"
)
nil
end
end
end
end
end
120 changes: 118 additions & 2 deletions instrumentation/base/test/instrumentation/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,80 @@ def initialize(*args)
_(instance.config).must_equal(expected_config)
end

describe 'when environment variables are used to set configuration options' do
after do
# Force re-install of instrumentation
instance.instance_variable_set(:@installed, false)
end

let(:env_controlled_instrumentation) do
Class.new(OpenTelemetry::Instrumentation::Base) do
instrumentation_name 'opentelemetry_instrumentation_env_controlled'
instrumentation_version '0.0.2'

present { true }
compatible { true }
install { true }

option(:first, default: 'first_default', validate: :string)
option(:second, default: :no, validate: %I[yes no maybe])
option(:third, default: 1, validate: ->(v) { v <= 10 })
option(:forth, default: false, validate: :boolean)
option(:fifth, default: true, validate: :boolean)
end
end

let(:instance) { env_controlled_instrumentation.instance }

it 'installs options defined by environment variable and overrides defaults' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENV_CONTROLLED_CONFIG_OPTS' => 'first=non_default_value') do
instance.install
_(instance.config).must_equal(first: 'non_default_value', second: :no, third: 1, forth: false, fifth: true)
end
end

it 'installs boolean type options defined by environment variable and only evalutes the lowercase string "true" to be truthy' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENV_CONTROLLED_CONFIG_OPTS' => 'first=non_default_value;forth=true;fifth=truthy') do
instance.install
_(instance.config).must_equal(first: 'non_default_value', second: :no, third: 1, forth: true, fifth: false)
end
end

it 'installs only enum options defined by environment variable that accept a symbol' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENV_CONTROLLED_CONFIG_OPTS' => 'second=maybe') do
instance.install
_(instance.config).must_equal(first: 'first_default', second: :maybe, third: 1, forth: false, fifth: true)
end
end

it 'installs options defined by environment variable and overrides local configuration' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENV_CONTROLLED_CONFIG_OPTS' => 'first=non_default_value') do
instance.install(first: 'another_default')
_(instance.config).must_equal(first: 'non_default_value', second: :no, third: 1, forth: false, fifth: true)
end
end

it 'installs multiple options defined by environment variable' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENV_CONTROLLED_CONFIG_OPTS' => 'first=non_default_value;second=maybe') do
instance.install(first: 'another_default', second: :yes)
_(instance.config).must_equal(first: 'non_default_value', second: :maybe, third: 1, forth: false, fifth: true)
end
end

it 'does not install callable options defined by environment variable' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENV_CONTROLLED_CONFIG_OPTS' => 'first=non_default_value;second=maybe;third=5') do
instance.install(first: 'another_default', second: :yes)
_(instance.config).must_equal(first: 'non_default_value', second: :maybe, third: 1, forth: false, fifth: true)
end
end
end

describe 'when there is an option with a raising validate callable' do
after do
# Force re-install of instrumentation
instance.instance_variable_set(:@installed, false)
end

let(:buggy_instrumentation) do
Class.new(OpenTelemetry::Instrumentation::Base) do
instrumentation_name 'test_buggy_instrumentation'
Expand All @@ -195,16 +268,59 @@ def initialize(*args)
install { true }

option :first, default: 'first_default', validate: ->(_v) { raise 'hell' }
option :second, default: 'second_default', validate: ->(_v) { true }
option :second, default: 'second_default', validate: ->(v) { v.is_a?(String) }
end
end

let(:instance) { buggy_instrumentation.instance }

it 'falls back to the default' do
instance = buggy_instrumentation.instance
instance.install(first: 'value', second: 'user_value')
_(instance.config).must_equal(first: 'first_default', second: 'user_value')
end
end

describe 'when there is an option with an enum validation type' do
after do
# Force re-install of instrumentation
instance.instance_variable_set(:@installed, false)
end

let(:enum_instrumentation) do
Class.new(OpenTelemetry::Instrumentation::Base) do
instrumentation_name 'opentelemetry_instrumentation_enum'
instrumentation_version '0.0.2'

present { true }
compatible { true }
install { true }

option(:first, default: :no, validate: %I[yes no maybe])
option(:second, default: :no, validate: %I[yes no maybe])
end
end

let(:instance) { enum_instrumentation.instance }

it 'falls back to the default if user option is not an enumerable option' do
instance.install(first: :yes, second: :perhaps)
_(instance.config).must_equal(first: :yes, second: :no)
end

it 'installs options defined by environment variable and overrides defaults and user config' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENUM_CONFIG_OPTS' => 'first=yes') do
instance.install(first: :maybe, second: :no)
_(instance.config).must_equal(first: :yes, second: :no)
end
end

it 'falls back to install options defined by user config when environment variable fails validation' do
with_env('OTEL_RUBY_INSTRUMENTATION_ENUM_CONFIG_OPTS' => 'first=perhaps') do
instance.install(first: :maybe, second: :no)
_(instance.config).must_equal(first: :maybe, second: :no)
end
end
end
end

describe 'when uninstallable' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base

option :peer_service, default: nil, validate: :string
option :enable_sql_obfuscation, default: false, validate: :boolean
option :db_statement, default: :include, validate: ->(opt) { %I[omit include obfuscate].include?(opt) }
option :db_statement, default: :include, validate: %I[omit include obfuscate]

private

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,73 @@
_(span.attributes['net.peer.port']).must_equal port.to_s
end
end

describe 'when db_statement is configured via environment variable' do
describe 'when db_statement set as omit' do
it 'omits db.statement attribute' do
with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_CONFIG_OPTS' => 'db_statement=omit;') do
instrumentation.instance_variable_set(:@installed, false)
instrumentation.install
sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'"
expect do
client.query(sql)
end.must_raise Mysql2::Error

_(span.attributes['db.system']).must_equal 'mysql'
_(span.attributes['db.name']).must_equal 'mysql'
_(span.name).must_equal 'select'
_(span.attributes).wont_include('db.statement')
_(span.attributes['net.peer.name']).must_equal host.to_s
_(span.attributes['net.peer.port']).must_equal port.to_s
end
end
end

describe 'when db_statement set as obfuscate' do
it 'obfuscates SQL parameters in db.statement' do
with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_CONFIG_OPTS' => 'db_statement=obfuscate;') do
instrumentation.instance_variable_set(:@installed, false)
instrumentation.install

sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'"
obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?'
expect do
client.query(sql)
end.must_raise Mysql2::Error

_(span.attributes['db.system']).must_equal 'mysql'
_(span.attributes['db.name']).must_equal 'mysql'
_(span.name).must_equal 'select'
_(span.attributes['db.statement']).must_equal obfuscated_sql
_(span.attributes['net.peer.name']).must_equal host.to_s
_(span.attributes['net.peer.port']).must_equal port.to_s
end
end
end

describe 'when db_statement is set differently than local config' do
let(:config) { { db_statement: :omit } }

it 'overrides local config and obfuscates SQL parameters in db.statement' do
with_env('OTEL_RUBY_INSTRUMENTATION_MYSQL2_CONFIG_OPTS' => 'db_statement=obfuscate') do
instrumentation.instance_variable_set(:@installed, false)
instrumentation.install

sql = "SELECT * from users where users.id = 1 and users.email = 'test@test.com'"
obfuscated_sql = 'SELECT * from users where users.id = ? and users.email = ?'
expect do
client.query(sql)
end.must_raise Mysql2::Error

_(span.attributes['db.system']).must_equal 'mysql'
_(span.attributes['db.name']).must_equal 'mysql'
_(span.name).must_equal 'select'
_(span.attributes['db.statement']).must_equal obfuscated_sql
_(span.attributes['net.peer.name']).must_equal host.to_s
_(span.attributes['net.peer.port']).must_equal port.to_s
end
end
end
end
end unless ENV['OMIT_SERVICES']
end
9 changes: 9 additions & 0 deletions instrumentation/mysql2/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@
OpenTelemetry::SDK.configure do |c|
c.add_span_processor span_processor
end

def with_env(new_env)
env_to_reset = ENV.select { |k, _| new_env.key?(k) }
keys_to_delete = new_env.keys - ENV.keys
new_env.each_pair { |k, v| ENV[k] = v }
yield
env_to_reset.each_pair { |k, v| ENV[k] = v }
keys_to_delete.each { |k| ENV.delete(k) }
end

0 comments on commit 9f760ee

Please sign in to comment.