diff --git a/Rakefile b/Rakefile index 68811527ecd..9b854bd1514 100644 --- a/Rakefile +++ b/Rakefile @@ -109,7 +109,8 @@ namespace :spec do 'patcher', 'registerable', 'registry', - 'registry/*' + 'registry/*', + 'propagation/**/*' ].join(',') t.pattern = "spec/**/contrib/{#{contrib_paths}}_spec.rb" diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 28c3e447ac7..0e573691962 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -1332,6 +1332,7 @@ client.query("SELECT * FROM users WHERE group='x'") | Key | Description | Default | | --- | ----------- | ------- | | `service_name` | Service name used for `mysql2` instrumentation | `'mysql2'` | +| `comment_propagation` | SQL comment propagation mode for database monitoring.
(example: `disabled` \| `service`).

**Important**: *Note that enabling sql comment propagation results in potentially confidential data (service names) being stored in the databases which can then be accessed by other 3rd parties that have been granted access to the database.* | `'disabled'` | ### Net/HTTP diff --git a/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb b/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb index b37228d1f0f..4bd8cac76e5 100644 --- a/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +++ b/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb @@ -3,6 +3,8 @@ require_relative '../../configuration/settings' require_relative '../ext' +require_relative '../../propagation/sql_comment' + module Datadog module Tracing module Contrib @@ -27,6 +29,16 @@ class Settings < Contrib::Configuration::Settings end option :service_name, default: Ext::DEFAULT_PEER_SERVICE_NAME + + option :comment_propagation do |o| + o.default do + ENV.fetch( + Contrib::Propagation::SqlComment::Ext::ENV_DBM_PROPAGATION_MODE, + Contrib::Propagation::SqlComment::Ext::DISABLED + ) + end + o.lazy + end end end end diff --git a/lib/datadog/tracing/contrib/mysql2/instrumentation.rb b/lib/datadog/tracing/contrib/mysql2/instrumentation.rb index 5843e6337d7..246be8afe31 100644 --- a/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +++ b/lib/datadog/tracing/contrib/mysql2/instrumentation.rb @@ -4,6 +4,8 @@ require_relative '../analytics' require_relative 'ext' require_relative '../ext' +require_relative '../propagation/sql_comment' +require_relative '../propagation/sql_comment/mode' module Datadog module Tracing @@ -39,6 +41,12 @@ def query(sql, options = {}) span.set_tag(Ext::TAG_DB_NAME, query_options[:database]) span.set_tag(Tracing::Metadata::Ext::NET::TAG_TARGET_HOST, query_options[:host]) span.set_tag(Tracing::Metadata::Ext::NET::TAG_TARGET_PORT, query_options[:port]) + + propagation_mode = Contrib::Propagation::SqlComment::Mode.new(comment_propagation) + + Contrib::Propagation::SqlComment.annotate!(span, propagation_mode) + sql = Contrib::Propagation::SqlComment.prepend_comment(sql, span, propagation_mode) + super(sql, options) end end @@ -56,6 +64,10 @@ def analytics_enabled? def analytics_sample_rate datadog_configuration[:analytics_sample_rate] end + + def comment_propagation + datadog_configuration[:comment_propagation] + end end end end diff --git a/lib/datadog/tracing/contrib/propagation/sql_comment.rb b/lib/datadog/tracing/contrib/propagation/sql_comment.rb new file mode 100644 index 00000000000..847b91decbc --- /dev/null +++ b/lib/datadog/tracing/contrib/propagation/sql_comment.rb @@ -0,0 +1,49 @@ +# typed: false + +require_relative 'sql_comment/comment' +require_relative 'sql_comment/ext' + +module Datadog + module Tracing + module Contrib + module Propagation + # Implements sql comment propagation related contracts. + module SqlComment + def self.annotate!(span_op, mode) + return unless mode.enabled? + + # PENDING: Until `traceparent`` implementation in `full` mode + # span_op.set_tag(Ext::TAG_DBM_TRACE_INJECTED, true) if mode.full? + end + + def self.prepend_comment(sql, span_op, mode) + return sql unless mode.enabled? + + tags = { + Ext::KEY_DATABASE_SERVICE => span_op.service, + Ext::KEY_ENVIRONMENT => datadog_configuration.env, + Ext::KEY_PARENT_SERVICE => datadog_configuration.service, + Ext::KEY_VERSION => datadog_configuration.version + } + + # PENDING: Until `traceparent`` implementation in `full` mode + # tags.merge!(trace_context(span_op)) if mode.full? + + "#{Comment.new(tags)} #{sql}" + end + + def self.datadog_configuration + Datadog.configuration + end + + # TODO: Derive from trace + def self.trace_context(_) + { + # traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' + }.freeze + end + end + end + end + end +end diff --git a/lib/datadog/tracing/contrib/propagation/sql_comment/comment.rb b/lib/datadog/tracing/contrib/propagation/sql_comment/comment.rb new file mode 100644 index 00000000000..bd788ee677f --- /dev/null +++ b/lib/datadog/tracing/contrib/propagation/sql_comment/comment.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# typed: false + +require 'erb' + +module Datadog + module Tracing + module Contrib + module Propagation + module SqlComment + # To be prepended to a sql statement. + class Comment + def initialize(hash) + @hash = hash + end + + def to_s + @string ||= begin + ret = String.new + + @hash.each do |key, value| + next if value.nil? + + # Url encode + value = ERB::Util.url_encode(value) + + # Escape SQL + ret << "#{key}='#{value}'," + end + + # Remove the last `,` + ret.chop! + + "/*#{ret}*/" + end + end + end + end + end + end + end +end diff --git a/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb b/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb new file mode 100644 index 00000000000..0a70804bf1c --- /dev/null +++ b/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb @@ -0,0 +1,32 @@ +# typed: false + +module Datadog + module Tracing + module Contrib + module Propagation + module SqlComment + module Ext + ENV_DBM_PROPAGATION_MODE = 'DD_DBM_PROPAGATION_MODE'.freeze + + # The default mode for sql comment propagation + DISABLED = 'disabled'.freeze + + # The `service` mode propagates service configuration + SERVICE = 'service'.freeze + + # The `full` mode propagates service configuration + trace context + FULL = 'full'.freeze + + # The value should be `true` when `full` mode + TAG_DBM_TRACE_INJECTED = '_dd.dbm_trace_injected'.freeze + + KEY_DATABASE_SERVICE = 'dddbs'.freeze + KEY_ENVIRONMENT = 'dde'.freeze + KEY_PARENT_SERVICE = 'ddps'.freeze + KEY_VERSION = 'ddpv'.freeze + end + end + end + end + end +end diff --git a/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb b/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb new file mode 100644 index 00000000000..3a91a30aaad --- /dev/null +++ b/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb @@ -0,0 +1,28 @@ +# typed: false + +require_relative 'ext' + +module Datadog + module Tracing + module Contrib + module Propagation + # Implements sql comment propagation related contracts. + module SqlComment + Mode = Struct.new(:mode) do + def enabled? + service? || full? + end + + def service? + mode == Ext::SERVICE + end + + def full? + mode == Ext::FULL + end + end + end + end + end + end +end diff --git a/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb b/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb index 868145b5851..2f39c93af79 100644 --- a/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +++ b/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb @@ -25,7 +25,7 @@ def initialize(app, opt = {}) def call(env) # Set the trace context (e.g. distributed tracing) if configuration[:distributed_tracing] && Tracing.active_trace.nil? - original_trace = Propagation::HTTP.extract(env) + original_trace = Tracing::Propagation::HTTP.extract(env) Tracing.continue_trace!(original_trace) end diff --git a/spec/datadog/tracing/contrib/mysql2/patcher_spec.rb b/spec/datadog/tracing/contrib/mysql2/patcher_spec.rb index b8fcf5c5472..1b30413716e 100644 --- a/spec/datadog/tracing/contrib/mysql2/patcher_spec.rb +++ b/spec/datadog/tracing/contrib/mysql2/patcher_spec.rb @@ -3,6 +3,8 @@ require 'datadog/tracing/contrib/integration_examples' require 'datadog/tracing/contrib/support/spec_helper' require 'datadog/tracing/contrib/analytics_examples' +require 'datadog/tracing/contrib/propagation/sql_comment' +require 'datadog/tracing/contrib/sql_comment_propagation_examples' require 'ddtrace' require 'mysql2' @@ -42,35 +44,43 @@ describe 'tracing' do describe '#query' do + subject(:query) { client.query(sql_statement) } + + let(:sql_statement) { 'SELECT 1' } + context 'when the tracer is disabled' do before { tracer.enabled = false } it 'does not write spans' do - client.query('SELECT 1') + query + expect(spans).to be_empty end end context 'when the client is configured directly' do - let(:service_override) { 'mysql-override' } + let(:service_name) { 'mysql-override' } before do - Datadog.configure_onto(client, service_name: service_override) - client.query('SELECT 1') + Datadog.configure_onto(client, service_name: service_name) end it 'produces a trace with service override' do + query + expect(spans.count).to eq(1) - expect(span.service).to eq(service_override) + expect(span.service).to eq(service_name) expect(span.get_tag('db.system')).to eq('mysql') - expect(span.get_tag(Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE)).to eq(service_override) + expect(span.get_tag(Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE)).to eq(service_name) end + + it_behaves_like 'with sql comment propagation', span_op_name: 'mysql2.query' end context 'when a successful query is made' do - before { client.query('SELECT 1') } - it 'produces a trace' do + query + expect(spans.count).to eq(1) expect(span.get_tag('mysql2.db.name')).to eq(database) expect(span.get_tag('out.host')).to eq(host) @@ -81,27 +91,37 @@ end it_behaves_like 'analytics for integration' do + before { query } let(:analytics_enabled_var) { Datadog::Tracing::Contrib::Mysql2::Ext::ENV_ANALYTICS_ENABLED } let(:analytics_sample_rate_var) { Datadog::Tracing::Contrib::Mysql2::Ext::ENV_ANALYTICS_SAMPLE_RATE } end it_behaves_like 'a peer service span' do + before { query } let(:peer_hostname) { host } end - it_behaves_like 'measured span for integration', false + it_behaves_like 'measured span for integration', false do + before { query } + end + + it_behaves_like 'with sql comment propagation', span_op_name: 'mysql2.query' end context 'when a failed query is made' do - before { expect { client.query('SELECT INVALID') }.to raise_error(Mysql2::Error) } + let(:sql_statement) { 'SELECT INVALID' } it 'traces failed queries' do + expect { query }.to raise_error(Mysql2::Error) + expect(spans.count).to eq(1) expect(span.status).to eq(1) expect(span.get_tag('db.system')).to eq('mysql') expect(span.get_tag('error.msg')) .to eq("Unknown column 'INVALID' in 'field list'") end + + it_behaves_like 'with sql comment propagation', span_op_name: 'mysql2.query', error: Mysql2::Error end end end diff --git a/spec/datadog/tracing/contrib/propagation/sql_comment/comment_spec.rb b/spec/datadog/tracing/contrib/propagation/sql_comment/comment_spec.rb new file mode 100644 index 00000000000..26a7976ad20 --- /dev/null +++ b/spec/datadog/tracing/contrib/propagation/sql_comment/comment_spec.rb @@ -0,0 +1,44 @@ +# typed: ignore + +require 'datadog/tracing/contrib/propagation/sql_comment/comment' + +RSpec.describe Datadog::Tracing::Contrib::Propagation::SqlComment::Comment do + describe '#to_s' do + [ + [ + { first_name: 'datadog', last_name: nil }, + "/*first_name='datadog'*/" + ], + [ + { first_name: 'data', last_name: 'dog' }, + "/*first_name='data',last_name='dog'*/" + ], + [ + { url_encode: 'DROP TABLE FOO' }, + "/*url_encode='DROP%20TABLE%20FOO'*/" + ], + [ + { route: '/polls 1000' }, + "/*route='%2Fpolls%201000'*/" + ], + [ + { escape_single_quote: "Dunkin' Donuts" }, + "/*escape_single_quote='Dunkin%27%20Donuts'*/" + ], + [ + { star: '*' }, + "/*star='%2A'*/" + ], + [ + { back_slash: '\\' }, + "/*back_slash='%5C'*/" + ], + [ + { equal_sign: '=' }, + "/*equal_sign='%3D'*/" + ] + ].each do |tags, comment| + it { expect(described_class.new(tags).to_s).to eq(comment) } + end + end +end diff --git a/spec/datadog/tracing/contrib/propagation/sql_comment/mode_spec.rb b/spec/datadog/tracing/contrib/propagation/sql_comment/mode_spec.rb new file mode 100644 index 00000000000..b89c87cbf29 --- /dev/null +++ b/spec/datadog/tracing/contrib/propagation/sql_comment/mode_spec.rb @@ -0,0 +1,47 @@ +# typed: ignore + +require 'datadog/tracing/contrib/propagation/sql_comment/mode' + +RSpec.describe Datadog::Tracing::Contrib::Propagation::SqlComment::Mode do + describe '#enabled?' do + [ + ['disabled', false], + ['service', true], + ['full', true], + ['undefined', false] + ].each do |string, result| + context "when given `#{string}`" do + subject { described_class.new(string).enabled? } + it { is_expected.to be result } + end + end + end + + describe '#service?' do + [ + ['disabled', false], + ['service', true], + ['full', false], + ['undefined', false] + ].each do |string, result| + context "when given `#{string}`" do + subject { described_class.new(string).service? } + it { is_expected.to be result } + end + end + end + + describe '#full?' do + [ + ['disabled', false], + ['service', false], + ['full', true], + ['undefined', false] + ].each do |string, result| + context "when given `#{string}`" do + subject { described_class.new(string).full? } + it { is_expected.to be result } + end + end + end +end diff --git a/spec/datadog/tracing/contrib/propagation/sql_comment_spec.rb b/spec/datadog/tracing/contrib/propagation/sql_comment_spec.rb new file mode 100644 index 00000000000..beda666033e --- /dev/null +++ b/spec/datadog/tracing/contrib/propagation/sql_comment_spec.rb @@ -0,0 +1,83 @@ +# typed: ignore + +require 'datadog/tracing/contrib/propagation/sql_comment' +require 'datadog/tracing/contrib/propagation/sql_comment/mode' + +RSpec.describe Datadog::Tracing::Contrib::Propagation::SqlComment do + let(:propagation_mode) { Datadog::Tracing::Contrib::Propagation::SqlComment::Mode.new(mode) } + let(:span_op) { Datadog::Tracing::SpanOperation.new('sql_comment_propagation_span', service: 'database_service') } + + describe '.annotate!' do + context 'when `disabled` mode' do + let(:mode) { 'disabled' } + + it do + described_class.annotate!(span_op, propagation_mode) + + expect(span_op.get_tag('_dd.dbm_trace_injected')).to be_nil + end + end + + context 'when `service` mode' do + let(:mode) { 'service' } + + it do + described_class.annotate!(span_op, propagation_mode) + + expect(span_op.get_tag('_dd.dbm_trace_injected')).to be_nil + end + end + + context 'when `full` mode', pending: 'until `traceparent` implement with `full` mode' do + let(:mode) { 'full' } + + it do + described_class.annotate!(span_op, propagation_mode) + + expect(span_op.get_tag('_dd.dbm_trace_injected')).to eq('true') + end + end + end + + describe '.prepend_comment' do + around do |example| + without_warnings { Datadog.configuration.reset! } + example.run + without_warnings { Datadog.configuration.reset! } + end + + before do + Datadog.configure do |c| + c.env = 'production' + c.service = "Traders' Joe" + c.version = '1.0.0' + end + end + + let(:sql_statement) { 'SELECT 1' } + + subject { described_class.prepend_comment(sql_statement, span_op, propagation_mode) } + + context 'when `disabled` mode' do + let(:mode) { 'disabled' } + + it { is_expected.to eq(sql_statement) } + end + + context 'when `service` mode' do + let(:mode) { 'service' } + + it do + is_expected.to eq( + "/*dddbs='database_service',dde='production',ddps='Traders%27%20Joe',ddpv='1.0.0'*/ #{sql_statement}" + ) + end + end + + context 'when `full` mode', pending: 'until `traceparent` implement with `full` mode' do + let(:mode) { 'full' } + + it { is_expected.to eq(sql_statement) } + end + end +end diff --git a/spec/datadog/tracing/contrib/sql_comment_propagation_examples.rb b/spec/datadog/tracing/contrib/sql_comment_propagation_examples.rb new file mode 100644 index 00000000000..77a3639a6d6 --- /dev/null +++ b/spec/datadog/tracing/contrib/sql_comment_propagation_examples.rb @@ -0,0 +1,73 @@ +# typed: ignore + +RSpec.shared_examples_for 'with sql comment propagation' do |span_op_name:, error: nil| + context 'when default `disabled`' do + it_behaves_like 'propagates with sql comment', mode: 'disabled', span_op_name: span_op_name, error: error do + let(:propagation_mode) { Datadog::Tracing::Contrib::Propagation::SqlComment::Mode.new('disabled') } + end + end + + context 'when ENV variable `DD_DBM_PROPAGATION_MODE` is provided' do + around do |example| + ClimateControl.modify( + 'DD_DBM_PROPAGATION_MODE' => 'service', + &example + ) + end + + it_behaves_like 'propagates with sql comment', mode: 'service', span_op_name: span_op_name, error: error do + let(:propagation_mode) { Datadog::Tracing::Contrib::Propagation::SqlComment::Mode.new('service') } + end + end + + %w[disabled service full].each do |mode| + context "when `comment_propagation` is configured to #{mode}" do + let(:configuration_options) do + { comment_propagation: mode, service_name: service_name } + end + + it_behaves_like 'propagates with sql comment', mode: mode, span_op_name: span_op_name, error: error do + let(:propagation_mode) { Datadog::Tracing::Contrib::Propagation::SqlComment::Mode.new(mode) } + end + end + end +end + +RSpec.shared_examples_for 'propagates with sql comment' do |mode:, span_op_name:, error: nil| + it "propagates with mode: #{mode}" do + expect(Datadog::Tracing::Contrib::Propagation::SqlComment::Mode) + .to receive(:new).with(mode).and_return(propagation_mode) + + if error + expect { subject }.to raise_error(error) + else + subject + end + end + + it 'decorates the span operation' do + expect(Datadog::Tracing::Contrib::Propagation::SqlComment).to receive(:annotate!).with( + a_span_operation_with(name: span_op_name), + propagation_mode + ) + if error + expect { subject }.to raise_error(error) + else + subject + end + end + + it 'prepends sql comment to the sql statement' do + expect(Datadog::Tracing::Contrib::Propagation::SqlComment).to receive(:prepend_comment).with( + sql_statement, + a_span_operation_with(name: span_op_name, service: service_name), + propagation_mode + ).and_call_original + + if error + expect { subject }.to raise_error(error) + else + subject + end + end +end diff --git a/spec/support/span_helpers.rb b/spec/support/span_helpers.rb index 2f50b91247a..641520cb367 100644 --- a/spec/support/span_helpers.rb +++ b/spec/support/span_helpers.rb @@ -121,4 +121,13 @@ def description_of(actual) end end end + + RSpec::Matchers.define :a_span_operation_with do |expected| + match do |actual| + actual.instance_of?(Datadog::Tracing::SpanOperation) && + expected.all? do |key, value| + actual.__send__(key) == value + end + end + end end