From a39feff581e03aa547533bcb5d7823c588b4fce3 Mon Sep 17 00:00:00 2001 From: Oleg Pudeyev Date: Fri, 17 Jul 2020 01:44:07 -0400 Subject: [PATCH] RUBY-2018 Add client metadata support for wrapping libraries --- docs/tutorials/ruby-driver-create-client.txt | 7 ++ lib/mongo/client.rb | 35 ++++++ lib/mongo/server/app_metadata.rb | 30 ++++- spec/mongo/client_construction_spec.rb | 112 +++++++++++++++++++ spec/mongo/server/app_metadata_shared.rb | 80 +++++++++++++ 5 files changed, 261 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/ruby-driver-create-client.txt b/docs/tutorials/ruby-driver-create-client.txt index 5608ea70fa..e07bddf157 100644 --- a/docs/tutorials/ruby-driver-create-client.txt +++ b/docs/tutorials/ruby-driver-create-client.txt @@ -613,6 +613,13 @@ Ruby Options - ``Float`` - 10 + * - ``:wrapping_libraries`` + - Information about libraries such as ODMs that are wrapping the driver. + Specify the lower level libraries first. Allowed hash keys: :name, + :version, :platform. Example: ``[name: 'Mongoid', version: '7.1.2']`` + - ``Array`` + - none + * - ``:write`` - Deprecated. Equivalent to ``:write_concern`` option. If both ``:write`` and ``:write_concern`` are specified, their values must be identical. diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index 9e25e91e1f..1adcd84bff 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -104,6 +104,7 @@ class Client :truncate_logs, :user, :wait_queue_timeout, + :wrapping_libraries, :write, :write_concern, :zlib_compression_level, @@ -375,6 +376,10 @@ def hash # @option options [ String ] :user The user name. # @option options [ Float ] :wait_queue_timeout The time to wait, in # seconds, in the connection pool for a connection to be checked in. + # @option options [ Array ] :wrapping_libraries Information about + # libraries such as ODMs that are wrapping the driver, to be added to + # metadata sent to the server. Specify the lower level libraries first. + # Allowed hash keys: :name, :version, :platform. # @option options [ Hash ] :write Deprecated. Equivalent to :write_concern # option. # @option options [ Hash ] :write_concern The write concern options. @@ -1160,6 +1165,36 @@ def validate_options!(addresses = nil) raise ArgumentError, ":bg_error_backtrace option value must be true, false, nil or a positive integer: #{value}" end end + + if libraries = options[:wrapping_libraries] + unless Array === libraries + raise ArgumentError, ":wrapping_libraries must be an array of hashes: #{libraries}" + end + + libraries = libraries.map do |library| + Utils.shallow_symbolize_keys(library) + end + + libraries.each do |library| + unless Hash === library + raise ArgumentError, ":wrapping_libraries element is not a hash: #{library}" + end + + if library.empty? + raise ArgumentError, ":wrapping_libraries element is empty" + end + + unless (library.keys - %i(name platform version)).empty? + raise ArgumentError, ":wrapping_libraries element has invalid keys (allowed keys: :name, :platform, :version): #{library}" + end + + library.each do |key, value| + if value.include?('|') + raise ArgumentError, ":wrapping_libraries element value cannot include '|': #{value}" + end + end + end + end end # Validates all authentication-related options after they are set on the client diff --git a/lib/mongo/server/app_metadata.rb b/lib/mongo/server/app_metadata.rb index 9afa1b25ab..59cb2184fa 100644 --- a/lib/mongo/server/app_metadata.rb +++ b/lib/mongo/server/app_metadata.rb @@ -65,12 +65,17 @@ class AppMetadata # the metadata printed to the mongod logs upon establishing a connection # in server versions >= 3.4. # @option options [ String ] :user The user name. + # @option options [ Array ] :wrapping_libraries Information about + # libraries such as ODMs that are wrapping the driver. Specify the + # lower level libraries first. Allowed hash keys: :name, :version, + # :platform. # # @since 2.4.0 def initialize(options) @app_name = options[:app_name].to_s if options[:app_name] @platform = options[:platform] @compressors = options[:compressors] || [] + @wrapping_libraries = options[:wrapping_libraries] if options[:user] && !options[:auth_mech] auth_db = options[:auth_source] || 'admin' @@ -78,6 +83,10 @@ def initialize(options) end end + # @return [ Array | nil ] Information about libraries wrapping + # the driver. + attr_reader :wrapping_libraries + # Get the bytes of the ismaster message including this metadata. # # @api private @@ -140,9 +149,17 @@ def document end def driver_doc + names = [DRIVER_NAME] + versions = [Mongo::VERSION] + if wrapping_libraries + wrapping_libraries.each do |library| + names << library[:name] || '' + versions << library[:version] || '' + end + end { - name: DRIVER_NAME, - version: Mongo::VERSION + name: names.join('|'), + version: versions.join('|'), } end @@ -175,12 +192,19 @@ def platform ruby_versions = ["Ruby #{RUBY_VERSION}"] platforms = [RUBY_PLATFORM] end - [ + platform = [ @platform, *ruby_versions, *platforms, RbConfig::CONFIG['build'], ].compact.join(', ') + platforms = [platform] + if wrapping_libraries + wrapping_libraries.each do |library| + platforms << library[:platform] || '' + end + end + platforms.join('|') end end end diff --git a/spec/mongo/client_construction_spec.rb b/spec/mongo/client_construction_spec.rb index 42eb101dd3..5abe1d94cf 100644 --- a/spec/mongo/client_construction_spec.rb +++ b/spec/mongo/client_construction_spec.rb @@ -1382,6 +1382,118 @@ end end =end + + context ':wrapping_libraries option' do + let(:options) do + {wrapping_libraries: wrapping_libraries} + end + + context 'valid input' do + context 'symbol keys' do + let(:wrapping_libraries) do + [name: 'Mongoid', version: '7.1.2'].freeze + end + + it 'works' do + client.options[:wrapping_libraries].should == ['name' => 'Mongoid', 'version' => '7.1.2'] + end + end + + context 'string keys' do + let(:wrapping_libraries) do + ['name' => 'Mongoid', 'version' => '7.1.2'].freeze + end + + it 'works' do + client.options[:wrapping_libraries].should == ['name' => 'Mongoid', 'version' => '7.1.2'] + end + end + + context 'Redacted keys' do + let(:wrapping_libraries) do + [Mongo::Options::Redacted.new(name: 'Mongoid', version: '7.1.2')].freeze + end + + it 'works' do + client.options[:wrapping_libraries].should == ['name' => 'Mongoid', 'version' => '7.1.2'] + end + end + + context 'two libraries' do + let(:wrapping_libraries) do + [ + {name: 'Mongoid', version: '7.1.2'}, + {name: 'Rails', version: '4.0', platform: 'Foobar'}, + ].freeze + end + + it 'works' do + client.options[:wrapping_libraries].should == [ + {'name' => 'Mongoid', 'version' => '7.1.2'}, + {'name' => 'Rails', 'version' => '4.0', 'platform' => 'Foobar'}, + ] + end + end + + context 'empty array' do + let(:wrapping_libraries) do + [] + end + + it 'works' do + client.options[:wrapping_libraries].should == [] + end + end + + context 'empty array' do + let(:wrapping_libraries) do + nil + end + + it 'works' do + client.options[:wrapping_libraries].should be nil + end + end + end + + context 'valid input' do + context 'hash given instead of an array' do + let(:wrapping_libraries) do + {name: 'Mongoid', version: '7.1.2'}.freeze + end + + it 'is rejected' do + lambda do + client + end.should raise_error(ArgumentError, /:wrapping_libraries must be an array of hashes/) + end + end + + context 'invalid keys' do + let(:wrapping_libraries) do + [name: 'Mongoid', invalid: '7.1.2'].freeze + end + + it 'is rejected' do + lambda do + client + end.should raise_error(ArgumentError, /:wrapping_libraries element has invalid keys/) + end + end + + context 'value includes |' do + let(:wrapping_libraries) do + [name: 'Mongoid|on|Rails', version: '7.1.2'].freeze + end + + it 'is rejected' do + lambda do + client + end.should raise_error(ArgumentError, /:wrapping_libraries element value cannot include '|'/) + end + end + end + end end context 'when making a block client' do diff --git a/spec/mongo/server/app_metadata_shared.rb b/spec/mongo/server/app_metadata_shared.rb index c43410d183..398276b7fa 100644 --- a/spec/mongo/server/app_metadata_shared.rb +++ b/spec/mongo/server/app_metadata_shared.rb @@ -53,4 +53,84 @@ end end end + + context 'when wrapping libraries are specified' do + let(:app_metadata) do + described_class.new(wrapping_libraries: wrapping_libraries) + end + + context 'one' do + let(:wrapping_libraries) { [wrapping_library] } + + context 'no fields' do + let(:wrapping_library) do + {} + end + + it 'adds empty strings' do + document[:client][:driver][:name].should == 'mongo-ruby-driver|' + document[:client][:driver][:version].should == "#{Mongo::VERSION}|" + document[:client][:platform].should =~ /\AJ?Ruby[^|]+\|\z/ + end + end + + context 'some fields' do + let(:wrapping_library) do + {name: 'Mongoid'} + end + + it 'adds the fields' do + document[:client][:driver][:name].should == 'mongo-ruby-driver|Mongoid' + document[:client][:driver][:version].should == "#{Mongo::VERSION}|" + document[:client][:platform].should =~ /\AJ?Ruby[^|]+\|\z/ + end + end + + context 'all fields' do + let(:wrapping_library) do + {name: 'Mongoid', version: '7.1.2', platform: 'OS9000'} + end + + it 'adds the fields' do + document[:client][:driver][:name].should == 'mongo-ruby-driver|Mongoid' + document[:client][:driver][:version].should == "#{Mongo::VERSION}|7.1.2" + document[:client][:platform].should =~ /\AJ?Ruby[^|]+\|OS9000\z/ + end + end + end + + context 'two' do + context 'some fields' do + let(:wrapping_libraries) do + [ + {name: 'Mongoid', version: '42'}, + # All libraries should be specifying their versions, in theory, + # but test not specifying a version. + {version: '4.0', platform: 'OS9000'}, + ] + end + + it 'adds the fields' do + document[:client][:driver][:name].should == 'mongo-ruby-driver|Mongoid|' + document[:client][:driver][:version].should == "#{Mongo::VERSION}|42|4.0" + document[:client][:platform].should =~ /\AJ?Ruby[^|]+\|\|OS9000\z/ + end + end + + context 'a realistic Mongoid & Rails wrapping' do + let(:wrapping_libraries) do + [ + {name: 'Mongoid', version: '7.1.2'}, + {name: 'Rails', version: '6.0.3'}, + ] + end + + it 'adds the fields' do + document[:client][:driver][:name].should == 'mongo-ruby-driver|Mongoid|Rails' + document[:client][:driver][:version].should == "#{Mongo::VERSION}|7.1.2|6.0.3" + document[:client][:platform].should =~ /\AJ?Ruby[^|]+\|\|\z/ + end + end + end + end end