diff --git a/Matrixfile b/Matrixfile index 90c803f3a92..82ee07ac127 100644 --- a/Matrixfile +++ b/Matrixfile @@ -14,6 +14,9 @@ 'core_with_libdatadog_api' => { '' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby', }, + 'core_with_rails' => { + 'rails8' => '❌ 2.5 / ❌ 2.6 / ❌ 2.7 / ❌ 3.0 / ❌ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby', + }, 'error_tracking' => { '' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby', }, diff --git a/Rakefile b/Rakefile index da19cb22b5d..2dab815f4e1 100644 --- a/Rakefile +++ b/Rakefile @@ -86,13 +86,13 @@ namespace :spec do :graphql, :graphql_unified_trace_patcher, :graphql_trace_patcher, :graphql_tracing_patcher, :rails, :railsredis, :railsredis_activesupport, :railsactivejob, :elasticsearch, :http, :redis, :sidekiq, :sinatra, :hanami, :hanami_autoinstrument, - :profiling, :core_with_libdatadog_api, :error_tracking, :open_feature] + :profiling, :core_with_libdatadog_api, :error_tracking, :open_feature, :core_with_rails] desc '' # "Explicitly hiding from `rake -T`" RSpec::Core::RakeTask.new(:main) do |t, args| t.pattern = 'spec/**/*_spec.rb' t.exclude_pattern = 'spec/**/{appsec/integration,contrib,benchmark,redis,auto_instrument,opentelemetry,open_feature,profiling,crashtracking,error_tracking,rubocop,data_streams}/**/*_spec.rb,' \ - ' spec/**/{auto_instrument,opentelemetry,process_discovery,stable_config,ddsketch,open_feature}_spec.rb,' \ + ' spec/**/{auto_instrument,opentelemetry,process_discovery,stable_config,ddsketch,open_feature,process}_spec.rb,' \ ' spec/datadog/gem_packaging_spec.rb' t.rspec_opts = args.to_a.join(' ') end @@ -233,6 +233,12 @@ namespace :spec do end # rubocop:enable Style/MultilineBlockChain + desc '' # "Explicitly hiding from `rake -T`" + RSpec::Core::RakeTask.new(:core_with_rails) do |t, args| + t.pattern = 'spec/datadog/core/environment/process_spec.rb' + t.rspec_opts = args.to_a.join(' ') + end + desc '' # "Explicitly hiding from `rake -T`" RSpec::Core::RakeTask.new(:error_tracking) do |t, args| t.pattern = 'spec/datadog/error_tracking/**/*_spec.rb' diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb index ad3249ccc55..9a6a6faeebc 100644 --- a/lib/datadog/core/configuration/settings.rb +++ b/lib/datadog/core/configuration/settings.rb @@ -1003,6 +1003,16 @@ def initialize(*_) end end + # Enable experimental process tags propagation such that payloads like spans contain the process tag. + # + # @default `DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + option :experimental_propagate_process_tags_enabled do |o| + o.env 'DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED' + o.default false + o.type :bool + end + # Tracer specific configuration starting with APM (e.g. DD_APM_TRACING_ENABLED). # @public_api settings :apm do diff --git a/lib/datadog/core/configuration/supported_configurations.rb b/lib/datadog/core/configuration/supported_configurations.rb index a7aa85387cb..d571e9694b9 100644 --- a/lib/datadog/core/configuration/supported_configurations.rb +++ b/lib/datadog/core/configuration/supported_configurations.rb @@ -44,6 +44,7 @@ module Configuration "DD_ERROR_TRACKING_HANDLED_ERRORS" => {version: ["A"]}, "DD_ERROR_TRACKING_HANDLED_ERRORS_INCLUDE" => {version: ["A"]}, "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED" => {version: ["A"]}, + "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED" => {version: ["A"]}, "DD_GIT_COMMIT_SHA" => {version: ["A"]}, "DD_GIT_REPOSITORY_URL" => {version: ["A"]}, "DD_HEALTH_METRICS_ENABLED" => {version: ["A"]}, diff --git a/lib/datadog/core/environment/ext.rb b/lib/datadog/core/environment/ext.rb index 0b01d9fc4fe..572fc81c3ad 100644 --- a/lib/datadog/core/environment/ext.rb +++ b/lib/datadog/core/environment/ext.rb @@ -33,8 +33,14 @@ module Ext LANG_INTERPRETER = "#{RUBY_ENGINE}-#{RUBY_PLATFORM}" LANG_PLATFORM = RUBY_PLATFORM LANG_VERSION = RUBY_VERSION + PROCESS_TYPE = 'script' # Out of the options [jar, script, class, executable], we consider Ruby to always be a script RUBY_ENGINE = ::RUBY_ENGINE # e.g. 'ruby', 'jruby', 'truffleruby' TAG_ENV = 'env' + TAG_ENTRYPOINT_BASEDIR = "entrypoint.basedir" + TAG_ENTRYPOINT_NAME = "entrypoint.name" + TAG_ENTRYPOINT_WORKDIR = "entrypoint.workdir" + TAG_ENTRYPOINT_TYPE = "entrypoint.type" + TAG_PROCESS_TAGS = "_dd.tags.process" TAG_SERVICE = 'service' TAG_VERSION = 'version' diff --git a/lib/datadog/core/environment/process.rb b/lib/datadog/core/environment/process.rb new file mode 100644 index 00000000000..a4d34eddcdb --- /dev/null +++ b/lib/datadog/core/environment/process.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require_relative 'ext' +require_relative '../tag_normalizer' + +module Datadog + module Core + module Environment + # Retrieves process level information such that it can be attached to various payloads + # + # @api private + module Process + # This method returns a key/value part of serialized tags in the format of k1:v1,k2:v2,k3:v3 + # @return [String] comma-separated normalized key:value pairs + def self.serialized + return @serialized if defined?(@serialized) + tags = [] + + begin + workdir = TagNormalizer.normalize(entrypoint_workdir.to_s, remove_digit_start_char: false) + tags << "#{Environment::Ext::TAG_ENTRYPOINT_WORKDIR}:#{workdir}" unless workdir.empty? + + entry_name = TagNormalizer.normalize(entrypoint_name.to_s, remove_digit_start_char: false) + tags << "#{Environment::Ext::TAG_ENTRYPOINT_NAME}:#{entry_name}" unless entry_name.empty? + + basedir = TagNormalizer.normalize(entrypoint_basedir.to_s, remove_digit_start_char: false) + tags << "#{Environment::Ext::TAG_ENTRYPOINT_BASEDIR}:#{basedir}" unless basedir.empty? + + tags << "#{Environment::Ext::TAG_ENTRYPOINT_TYPE}:#{TagNormalizer.normalize(entrypoint_type, remove_digit_start_char: false)}" + rescue => e + Datadog.logger.debug("failed to get process_tags: #{e.class}: #{e}") + end + @serialized = tags.join(',').freeze + end + + # Returns the last segment of the working directory of the process + # Example: /app/myapp -> myapp + # @return [String] the last segment of the working directory + def self.entrypoint_workdir + File.basename(Dir.pwd) + end + + # Returns the entrypoint type of the process + # In Ruby, the entrypoint type is always 'script' + # @return [String] the type of the process, which is fixed in Ruby + def self.entrypoint_type + Environment::Ext::PROCESS_TYPE + end + + # Returns the last segment of the base directory of the process + # Example 1: /bin/mybin -> mybin + # Example 2: ruby /test/myapp.rb -> myapp + # @return [String] the last segment of base directory of the script + def self.entrypoint_name + File.basename($0) + end + + # Returns the last segment of the base directory of the process + # Example 1: /bin/mybin -> bin + # Example 2: ruby /test/myapp.js -> test + # @return [String] the last segment of the base directory of the script + def self.entrypoint_basedir + File.basename(File.expand_path(File.dirname($0))) + end + + private_class_method :entrypoint_workdir, :entrypoint_type, :entrypoint_name, :entrypoint_basedir + end + end + end +end diff --git a/lib/datadog/core/tag_normalizer.rb b/lib/datadog/core/tag_normalizer.rb new file mode 100644 index 00000000000..f2cab0a85f6 --- /dev/null +++ b/lib/datadog/core/tag_normalizer.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require_relative 'utils' + +module Datadog + module Core + # @api private + module TagNormalizer + # Normalization logic used for tag keys and values that the Trace Agent has for traces + # Useful for ensuring that tag keys and values are normalized consistently + # An use case for now is Process Tags which need to be sent across various intakes (profiling, tracing, etc.) consistently + + module_function + + INVALID_TAG_CHARACTERS = %r{[^\p{L}0-9_\-:./]} + LEADING_INVALID_CHARS_NO_DIGITS = %r{\A[^\p{L}:]++} + LEADING_INVALID_CHARS_WITH_DIGITS = %r{\A[^\p{L}0-9:./]++} + MAX_BYTE_SIZE = 200 # Represents the max tag length + VALID_ASCII_TAG = %r{\A[a-z:][a-z0-9:./-]*\z} + + # Based on https://github.com/DataDog/datadog-agent/blob/45799c842bbd216bcda208737f9f11cade6fdd95/pkg/trace/traceutil/normalize.go#L131 + # Specifically: + # - Must be valid UTF-8 + # - Invalid characters are replaced with an underscore + # - Leading non-letter characters are removed but colons are kept + # - Trailing non-letter characters are removed + # - Trailing underscores are removed + # - Consecutive underscores are merged into a single underscore + # - Maximum length is 200 characters + # If it's a tag value, allow it to start with a digit + # @param original_value [String] The original string + # @param remove_digit_start_char [Boolean] - whether to remove the leading digit (currently only used for tag values) + # @return [String] The normalized string + def self.normalize(original_value, remove_digit_start_char: false) + # DEV-3.0: Ideally this encode call should be replaced with Datadog::Core::Utils.utf8_encode once it + # is safe to modify the default behavior. + value = original_value.to_s.encode('UTF-8', invalid: :replace, undef: :replace) + value.strip! + return "" if value.empty? + + return value if value.bytesize <= MAX_BYTE_SIZE && + value.match?(VALID_ASCII_TAG) + + if value.bytesize > MAX_BYTE_SIZE + value = value.byteslice(0, MAX_BYTE_SIZE) + value.scrub!("") + end + + value.downcase! + value.gsub!(INVALID_TAG_CHARACTERS, '_') + + # The Trace Agent allows tag values to start with a number so this logic is here too + leading_invalid_regex = remove_digit_start_char ? LEADING_INVALID_CHARS_NO_DIGITS : LEADING_INVALID_CHARS_WITH_DIGITS + value.sub!(leading_invalid_regex, "") + + value.squeeze!('_') if value.include?('__') + value.delete_suffix!('_') + + value + end + end + end +end diff --git a/lib/datadog/core/utils.rb b/lib/datadog/core/utils.rb index c0db770c117..b5e82637329 100644 --- a/lib/datadog/core/utils.rb +++ b/lib/datadog/core/utils.rb @@ -38,6 +38,8 @@ def self.truncate(value, size, omission = '...') # Ensure `str` is a valid UTF-8, ready to be # sent through the tracer transport. + # DEV-3.0: This method should unconditionally handle invalid byte sequences + # DEV-3.0: and return a safe string to display. # # @param [String,#to_s] str object to be converted to a UTF-8 string # @param [Boolean] binary whether to expect binary data in the `str` parameter diff --git a/lib/datadog/tracing/configuration/ext.rb b/lib/datadog/tracing/configuration/ext.rb index b86fc662418..1ab17e6a213 100644 --- a/lib/datadog/tracing/configuration/ext.rb +++ b/lib/datadog/tracing/configuration/ext.rb @@ -15,6 +15,7 @@ module Ext ENV_NATIVE_SPAN_EVENTS = 'DD_TRACE_NATIVE_SPAN_EVENTS' ENV_RESOURCE_RENAMING_ENABLED = 'DD_TRACE_RESOURCE_RENAMING_ENABLED' ENV_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT = 'DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT' + ENV_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED = 'DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED' # @public_api module SpanAttributeSchema diff --git a/lib/datadog/tracing/transport/trace_formatter.rb b/lib/datadog/tracing/transport/trace_formatter.rb index 48e733de951..d645b6879a6 100644 --- a/lib/datadog/tracing/transport/trace_formatter.rb +++ b/lib/datadog/tracing/transport/trace_formatter.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../../core/environment/identity' +require_relative '../../core/environment/process' require_relative '../../core/environment/socket' require_relative '../../core/environment/git' require_relative '../../core/git/ext' @@ -62,6 +63,7 @@ def format! tag_apm_tracing_disabled! if first_span + tag_process_tags! tag_git_repository_url! tag_git_commit_sha! end @@ -215,6 +217,15 @@ def tag_git_commit_sha! first_span.set_tag(Core::Git::Ext::TAG_COMMIT_SHA, git_commit_sha) end + def tag_process_tags! + return unless Datadog.configuration.experimental_propagate_process_tags_enabled + + first_span.set_tag( + Core::Environment::Ext::TAG_PROCESS_TAGS, + Core::Environment::Process.serialized + ) + end + private def partial? diff --git a/sig/datadog/core/environment/ext.rbs b/sig/datadog/core/environment/ext.rbs index 2d5fe0a9851..53032679975 100644 --- a/sig/datadog/core/environment/ext.rbs +++ b/sig/datadog/core/environment/ext.rbs @@ -37,6 +37,18 @@ module Datadog TAG_SERVICE: String TAG_VERSION: String + + PROCESS_TYPE: ::String + + TAG_ENTRYPOINT_BASEDIR: ::String + + TAG_ENTRYPOINT_NAME: ::String + + TAG_ENTRYPOINT_WORKDIR: ::String + + TAG_ENTRYPOINT_TYPE: ::String + + TAG_PROCESS_TAGS: ::String end end end diff --git a/sig/datadog/core/environment/process.rbs b/sig/datadog/core/environment/process.rbs new file mode 100644 index 00000000000..828ca6fe163 --- /dev/null +++ b/sig/datadog/core/environment/process.rbs @@ -0,0 +1,21 @@ +module Datadog + module Core + module Environment + module Process + @serialized: ::String + + def self.serialized: () -> ::String + + private + + def self.entrypoint_workdir: () -> ::String + + def self.entrypoint_type: () -> ::String + + def self.entrypoint_name: () -> ::String + + def self.entrypoint_basedir: () -> ::String + end + end + end +end diff --git a/sig/datadog/core/tag_normalizer.rbs b/sig/datadog/core/tag_normalizer.rbs new file mode 100644 index 00000000000..d13d09ec621 --- /dev/null +++ b/sig/datadog/core/tag_normalizer.rbs @@ -0,0 +1,14 @@ +module Datadog + module Core + module TagNormalizer + INVALID_TAG_CHARACTERS: ::Regexp + LEADING_INVALID_CHARS_NO_DIGITS: ::Regexp + LEADING_INVALID_CHARS_WITH_DIGITS: ::Regexp + MAX_BYTE_SIZE: ::Integer + VALID_ASCII_TAG: ::Regexp + + def self.normalize: (untyped original_value, ?remove_digit_start_char: bool) -> ::String + end + end +end + diff --git a/spec/datadog/core/configuration/settings_spec.rb b/spec/datadog/core/configuration/settings_spec.rb index 0e6ea0ae330..f7e8d6ee46a 100644 --- a/spec/datadog/core/configuration/settings_spec.rb +++ b/spec/datadog/core/configuration/settings_spec.rb @@ -1337,6 +1337,45 @@ end end + describe '#experimental_propagate_process_tags_enabled' do + subject(:experimental_propagate_process_tags_enabled) { settings.experimental_propagate_process_tags_enabled } + + context "when #{Datadog::Core::Environment::Ext::ENV_VERSION}" do + around do |example| + ClimateControl.modify('DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED' => environment) do + example.run + end + end + + context 'by default' do + let(:environment) { nil } + + it { is_expected.to be false } + end + + context 'when set to true' do + let(:environment) { 'true' } + + it { is_expected.to be true } + end + + context 'when set to false' do + let(:environment) { 'false' } + + it { is_expected.to be false } + end + end + end + + describe '#experimental_propagate_process_tags_enabled=' do + it 'updates the #experimental_propagate_process_tags_enabled setting' do + expect { settings.experimental_propagate_process_tags_enabled = true } + .to change { settings.experimental_propagate_process_tags_enabled } + .from(false) + .to(true) + end + end + describe '#time_now_provider=' do subject(:set_time_now_provider) { settings.time_now_provider = time_now_provider } diff --git a/spec/datadog/core/environment/process_spec.rb b/spec/datadog/core/environment/process_spec.rb new file mode 100644 index 00000000000..b32c220e05e --- /dev/null +++ b/spec/datadog/core/environment/process_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' +require 'datadog/core/environment/process' +require 'open3' + +RSpec.describe Datadog::Core::Environment::Process do + describe '::serialized' do + subject(:serialized) { described_class.serialized } + + def with_program_name(value) + original_0 = $0 + $0 = value + reset_serialized! + + yield + ensure + $0 = original_0 + reset_serialized! + end + + def reset_serialized! + described_class.remove_instance_variable(:@serialized) if described_class.instance_variable_defined?(:@serialized) + end + + it { is_expected.to be_a_kind_of(String) } + + it 'returns the same object when called multiple times' do + # Processes are fixed so no need to recompute this on each call + first_call = described_class.serialized + second_call = described_class.serialized + expect(first_call).to equal(second_call) + end + + it 'uses the basedir for /expectedbasedir/executable' do + with_program_name('/expectedbasedir/executable') do + expect(described_class.serialized).to include('entrypoint.workdir:app') + expect(described_class.serialized).to include('entrypoint.name:executable') + expect(described_class.serialized).to include('entrypoint.basedir:expectedbasedir') + expect(described_class.serialized).to include('entrypoint.type:script') + end + end + + it 'uses the basedir for irb' do + with_program_name('irb') do + expect(described_class.serialized).to include('entrypoint.workdir:app') + expect(described_class.serialized).to include('entrypoint.name:irb') + expect(described_class.serialized).to include('entrypoint.basedir:app') + expect(described_class.serialized).to include('entrypoint.type:script') + end + end + + it 'uses the basedir for my/path/rubyapp.rb' do + with_program_name('my/path/rubyapp.rb') do + expect(described_class.serialized).to include('entrypoint.workdir:app') + expect(described_class.serialized).to include('entrypoint.name:rubyapp.rb') + expect(described_class.serialized).to include('entrypoint.basedir:path') + expect(described_class.serialized).to include('entrypoint.type:script') + end + end + + it 'uses the basedir for bin/rails s' do + with_program_name('bin/rails s') do + expect(described_class.serialized).to include('entrypoint.workdir:app') + expect(described_class.serialized).to include('entrypoint.name:rails_s') + expect(described_class.serialized).to include('entrypoint.basedir:bin') + expect(described_class.serialized).to include('entrypoint.type:script') + end + end + end + + describe 'Scenario: Real applications' do + skip_unless_integration_testing_enabled + + context 'when running a real Rails application' do + it 'detects Rails process information correctly' do + project_root_directory = Dir.pwd + + Dir.mktmpdir do |tmp_dir| + Dir.chdir(tmp_dir) do + Bundler.with_unbundled_env do + _, _, _ = Open3.capture3('rails new test@_app --minimal --skip-active-record --skip-test --skip-keeps --skip-git --skip-docker') + expect(File.exist?("test@_app/Gemfile")).to be true + end + + File.open("test@_app/Gemfile", 'a') do |file| + file.puts "gem 'datadog', path: '#{project_root_directory}', require: false" + end + File.write("test@_app/config/initializers/process_initializer.rb", <<-RUBY) + Rails.application.config.after_initialize do + require 'datadog/core/environment/process' + STDERR.puts "_dd.tags.process:\#{Datadog::Core::Environment::Process.serialized}" + STDERR.flush + Thread.new { Process.kill('TERM', Process.pid) } + end + RUBY + + Bundler.with_unbundled_env do + Dir.chdir("test@_app") do + _, _, _ = Open3.capture3('bundle install') + _, err, _ = Open3.capture3('bundle exec rails s') + expect(err).to include('entrypoint.workdir:test_app') + expect(err).to include('entrypoint.type:script') + expect(err).to include('entrypoint.name:rails') + expect(err).to include('entrypoint.basedir:bin') + end + end + end + end + end + end + end +end diff --git a/spec/datadog/core/tag_normalizer_spec.rb b/spec/datadog/core/tag_normalizer_spec.rb new file mode 100644 index 00000000000..d31538fab05 --- /dev/null +++ b/spec/datadog/core/tag_normalizer_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' +require 'datadog/core/tag_normalizer' + +RSpec.describe Datadog::Core::TagNormalizer do + describe 'Follows the normalization logic from the Trace Agent for tag keys' do + # Test cases from the Trace Agent for consistency + # https://github.com/DataDog/datadog-agent/blob/45799c842bbd216bcda208737f9f11cade6fdd95/pkg/trace/traceutil/normalize_test.go#L17 + test_cases = [ + {in: '#test_starting_hash', out: 'test_starting_hash'}, + {in: 'TestCAPSandSuch', out: 'testcapsandsuch'}, + {in: 'Test Conversion Of Weird !@#$%^&**() Characters', out: 'test_conversion_of_weird_characters'}, + {in: '$#weird_starting', out: 'weird_starting'}, + {in: 'allowed:c0l0ns', out: 'allowed:c0l0ns'}, + {in: '1love', out: 'love'}, + {in: 'ünicöde', out: 'ünicöde'}, + {in: 'ünicöde:metäl', out: 'ünicöde:metäl'}, + {in: 'Data🐨dog🐶 繋がっ⛰てて', out: 'data_dog_繋がっ_てて'}, + {in: ' spaces ', out: 'spaces'}, + {in: ' #hashtag!@#spaces #__<># ', out: 'hashtag_spaces'}, + {in: ':testing', out: ':testing'}, + {in: '_foo', out: 'foo'}, + {in: ':::test', out: ':::test'}, + {in: 'contiguous_____underscores', out: 'contiguous_underscores'}, + {in: 'foo_', out: 'foo'}, + {in: '', out: ''}, + {in: ' ', out: ''}, + {in: 'ok', out: 'ok'}, + {in: 'AlsO:ök', out: 'also:ök'}, + {in: ':still_ok', out: ':still_ok'}, + {in: '___trim', out: 'trim'}, + {in: '12.:trim@', out: ':trim'}, + {in: '12.:trim@@', out: ':trim'}, + {in: 'fun:ky__tag/1', out: 'fun:ky_tag/1'}, + {in: 'fun:ky@tag/2', out: 'fun:ky_tag/2'}, + {in: 'fun:ky@@@tag/3', out: 'fun:ky_tag/3'}, + {in: 'tag:1/2.3', out: 'tag:1/2.3'}, + {in: '---fun:k####y_ta@#g/1_@@#', out: 'fun:k_y_ta_g/1'}, + {in: 'AlsO:œ#@ö))œk', out: 'also:œ_ö_œk'}, + {in: "test\x99\x8faaa", out: 'test_aaa'}, + {in: "test\x99\x8f", out: 'test'}, + {in: 'a' * 888, out: 'a' * 200}, + {in: ' regulartag ', out: 'regulartag'}, + {in: "\u017Fodd_\u017Fcase\u017F", out: "\u017Fodd_\u017Fcase\u017F"}, + {in: '™Ö™Ö™™Ö™', out: 'ö_ö_ö'}, + {in: "a�", out: 'a'}, + {in: "a��", out: 'a'}, + {in: "a��b", out: 'a_b'}, + {in: 'a' + ('🐶' * 799) + 'b', out: 'a'}, + # This test case doesn't work with the current logic because it yields 202 characters + # {in: 'A' + ('0' * 200) + ' ' + ('0' * 11), out: 'a' + ('0' * 200) + '_0'}, + ] + + test_cases.each do |test_case| + it "normalizes #{test_case[:in].inspect} to #{test_case[:out].inspect} like the Trace Agent" do + expect(described_class.normalize(test_case[:in], remove_digit_start_char: true)).to eq(test_case[:out]) + end + end + end + + describe 'Follows the normalization logic from the Trace Agent for tag values' do + test_cases = [ + # Reusing the same Trace Agent inputs, except a few of the outputs have changed + {in: '#test_starting_hash', out: 'test_starting_hash'}, + {in: 'TestCAPSandSuch', out: 'testcapsandsuch'}, + {in: 'Test Conversion Of Weird !@#$%^&**() Characters', out: 'test_conversion_of_weird_characters'}, + {in: '$#weird_starting', out: 'weird_starting'}, + {in: 'allowed:c0l0ns', out: 'allowed:c0l0ns'}, + {in: '1love', out: '1love'}, # differs when remove_digit_start_char is false + {in: 'ünicöde', out: 'ünicöde'}, + {in: 'ünicöde:metäl', out: 'ünicöde:metäl'}, + {in: 'Data🐨dog🐶 繋がっ⛰てて', out: 'data_dog_繋がっ_てて'}, + {in: ' spaces ', out: 'spaces'}, + {in: ' #hashtag!@#spaces #__<># ', out: 'hashtag_spaces'}, + {in: ':testing', out: ':testing'}, + {in: '_foo', out: 'foo'}, + {in: ':::test', out: ':::test'}, + {in: 'contiguous_____underscores', out: 'contiguous_underscores'}, + {in: 'foo_', out: 'foo'}, + {in: '', out: ''}, + {in: ' ', out: ''}, + {in: 'ok', out: 'ok'}, + {in: 'AlsO:ök', out: 'also:ök'}, + {in: ':still_ok', out: ':still_ok'}, + {in: '___trim', out: 'trim'}, + {in: '12.:trim@', out: '12.:trim'}, # differs when remove_digit_start_char is false + {in: '12.:trim@@', out: '12.:trim'}, # differs when remove_digit_start_char is false + {in: 'fun:ky__tag/1', out: 'fun:ky_tag/1'}, + {in: 'fun:ky@tag/2', out: 'fun:ky_tag/2'}, + {in: 'fun:ky@@@tag/3', out: 'fun:ky_tag/3'}, + {in: 'tag:1/2.3', out: 'tag:1/2.3'}, + {in: '---fun:k####y_ta@#g/1_@@#', out: 'fun:k_y_ta_g/1'}, + {in: 'AlsO:œ#@ö))œk', out: 'also:œ_ö_œk'}, + {in: "test\x99\x8faaa", out: 'test_aaa'}, + {in: "test\x99\x8f", out: 'test'}, + {in: 'a' * 888, out: 'a' * 200}, + {in: ' regulartag ', out: 'regulartag'}, + {in: "\u017Fodd_\u017Fcase\u017F", out: "\u017Fodd_\u017Fcase\u017F"}, + {in: '™Ö™Ö™™Ö™', out: 'ö_ö_ö'}, + {in: "a�", out: 'a'}, + {in: "a��", out: 'a'}, + {in: "a��b", out: 'a_b'}, + {in: 'a' + ('🐶' * 799) + 'b', out: 'a'}, + ] + + test_cases.each do |test_case| + it "normalizes #{test_case[:in].inspect} to #{test_case[:out].inspect} like the Trace Agent" do + # These test cases are from the Trace Agent's default normalize() behavior (tag keys) + expect(described_class.normalize(test_case[:in], remove_digit_start_char: false)).to eq(test_case[:out]) + end + end + end +end diff --git a/spec/datadog/core/utils_spec.rb b/spec/datadog/core/utils_spec.rb index 22faab09483..92ea26a9fbb 100644 --- a/spec/datadog/core/utils_spec.rb +++ b/spec/datadog/core/utils_spec.rb @@ -96,6 +96,22 @@ end end end + + context 'with valid and invalid characters in the string' do + let(:str) { "test\x99\x8faaa".force_encoding(Encoding::ASCII_8BIT) } + + it 'returns an empty string' do + is_expected.to eq(Datadog::Core::Utils::EMPTY_STRING) + end + end + + context 'with Unicode characters' do + let(:str) { 'ünicöde' } + + it 'preserves the original string' do + is_expected.to eq(str) + end + end end describe '.without_warnings' do diff --git a/spec/datadog/tracing/transport/trace_formatter_spec.rb b/spec/datadog/tracing/transport/trace_formatter_spec.rb index 3c0bb34cd74..4007dca2963 100644 --- a/spec/datadog/tracing/transport/trace_formatter_spec.rb +++ b/spec/datadog/tracing/transport/trace_formatter_spec.rb @@ -237,6 +237,35 @@ end end + shared_examples 'spans with process tags' do + it 'the first span has process tags' do + format! + expect(first_span.meta).to include(Datadog::Core::Environment::Ext::TAG_PROCESS_TAGS) + expect(first_span.meta[Datadog::Core::Environment::Ext::TAG_PROCESS_TAGS]).to eq(Datadog::Core::Environment::Process.serialized) + end + + it 'does not add process tags to non first spans' do + format! + trace.spans.each_with_index do |span, index| + if index == 0 + expect(span.meta).to include(Datadog::Core::Environment::Ext::TAG_PROCESS_TAGS) + expect(span.meta[Datadog::Core::Environment::Ext::TAG_PROCESS_TAGS]).to eq(Datadog::Core::Environment::Process.serialized) + else + expect(span.meta).to_not include(Datadog::Core::Environment::Ext::TAG_PROCESS_TAGS) + end + end + end + end + + shared_examples 'spans without process tags' do + it 'does not add process tags to any spans' do + format! + trace.spans.each do |span| + expect(span.meta).to_not include(Datadog::Core::Environment::Ext::TAG_PROCESS_TAGS) + end + end + end + context 'with no root span' do include_context 'no root span' @@ -284,6 +313,18 @@ include_context 'no git metadata' it_behaves_like 'first span with no git metadata' end + + context 'with process tags enabled' do + before do + allow(Datadog.configuration).to receive(:experimental_propagate_process_tags_enabled).and_return(true) + end + it_behaves_like 'spans with process tags' + end + + context 'without process tags enabled' do + # default is false + it_behaves_like 'spans without process tags' + end end context 'with missing root span' do @@ -333,6 +374,18 @@ include_context 'no git metadata' it_behaves_like 'first span with no git metadata' end + + context 'with process tags enabled' do + before do + allow(Datadog.configuration).to receive(:experimental_propagate_process_tags_enabled).and_return(true) + end + it_behaves_like 'spans with process tags' + end + + context 'without process tags enabled' do + # default is false + it_behaves_like 'spans without process tags' + end end context 'with a root span' do @@ -384,6 +437,18 @@ include_context 'no git metadata' it_behaves_like 'first span with no git metadata' end + + context 'with process tags enabled' do + before do + allow(Datadog.configuration).to receive(:experimental_propagate_process_tags_enabled).and_return(true) + end + it_behaves_like 'spans with process tags' + end + + context 'without process tags enabled' do + # default is false + it_behaves_like 'spans without process tags' + end end end end diff --git a/supported-configurations.json b/supported-configurations.json index 5888fd7b377..bf4383f4611 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -112,6 +112,9 @@ "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": { "version": ["A"] }, + "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": { + "version": ["A"] + }, "DD_GIT_COMMIT_SHA": { "version": ["A"] },