diff --git a/lib/datadog/core/environment/process.rb b/lib/datadog/core/environment/process.rb index a5a9664bbf3..7e3c6ca772b 100644 --- a/lib/datadog/core/environment/process.rb +++ b/lib/datadog/core/environment/process.rb @@ -14,6 +14,14 @@ module Process # @return [String] comma-separated normalized key:value pairs def self.serialized return @serialized if defined?(@serialized) + + @serialized = tags.join(',').freeze + end + + # This method returns an array in the format ["k1:v1","k2:v2","k3:v3"] + # @return [Array] array of normalized key:value pairs + def self.tags + return @tags if defined?(@tags) tags = [] workdir = TagNormalizer.normalize_process_value(entrypoint_workdir.to_s) @@ -27,7 +35,7 @@ def self.serialized tags << "#{Environment::Ext::TAG_ENTRYPOINT_TYPE}:#{TagNormalizer.normalize(entrypoint_type, remove_digit_start_char: false)}" - @serialized = tags.join(',').freeze + @tags = tags.freeze end # Returns the last segment of the working directory of the process diff --git a/lib/datadog/core/remote/client.rb b/lib/datadog/core/remote/client.rb index 84f3bc0b35e..6ae632fa931 100644 --- a/lib/datadog/core/remote/client.rb +++ b/lib/datadog/core/remote/client.rb @@ -161,6 +161,11 @@ def payload # standard:disable Metrics/MethodLength client_tracer[:app_version] = app_version if app_version + if Datadog.configuration.experimental_propagate_process_tags_enabled + process_tags = Core::Environment::Process.tags + client_tracer[:process_tags] = process_tags if process_tags.any? + end + { client: { state: { diff --git a/sig/datadog/core/environment/process.rbs b/sig/datadog/core/environment/process.rbs index 828ca6fe163..430e974329d 100644 --- a/sig/datadog/core/environment/process.rbs +++ b/sig/datadog/core/environment/process.rbs @@ -3,9 +3,12 @@ module Datadog module Environment module Process @serialized: ::String + @tags: Array[::String] def self.serialized: () -> ::String + def self.tags: () -> ::Array[::String] + private def self.entrypoint_workdir: () -> ::String diff --git a/spec/datadog/core/environment/process_spec.rb b/spec/datadog/core/environment/process_spec.rb index 02489d42af1..7c4f681a024 100644 --- a/spec/datadog/core/environment/process_spec.rb +++ b/spec/datadog/core/environment/process_spec.rb @@ -6,32 +6,6 @@ describe '::serialized' do subject(:serialized) { described_class.serialized } - def reset_serialized! - described_class.remove_instance_variable(:@serialized) if described_class.instance_variable_defined?(:@serialized) - end - - shared_context 'with mocked process environment' do - let(:pwd) { '/app' } - - around do |example| - @original_0 = $0 - $0 = program_name - example.run - $0 = @original_0 - end - - before do - allow(Dir).to receive(:pwd).and_return(pwd) - allow(File).to receive(:expand_path).and_call_original - allow(File).to receive(:expand_path).with('.').and_return('/app') - reset_serialized! - end - - after do - reset_serialized! - end - end - it { is_expected.to be_a_kind_of(String) } it 'returns the same object when called multiple times' do @@ -147,4 +121,85 @@ def reset_serialized! end end end + describe '::tags' do + subject(:tags) { described_class.tags } + + it { is_expected.to be_a_kind_of(Array) } + + it 'is an array of strings' do + expect(tags).to all(be_a(String)) + end + + 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.tags + second_call = described_class.tags + expect(first_call).to equal(second_call) + end + + context 'with /expectedbasedir/executable' do + include_context 'with mocked process environment' + let(:program_name) { '/expectedbasedir/executable' } + + it 'extracts out the tag array correctly' do + expect(tags.length).to eq(4) + expect(described_class.tags).to include('entrypoint.workdir:app') + expect(described_class.tags).to include('entrypoint.name:executable') + expect(described_class.tags).to include('entrypoint.basedir:expectedbasedir') + expect(described_class.tags).to include('entrypoint.type:script') + end + end + + context 'with irb' do + include_context 'with mocked process environment' + let(:program_name) { 'irb' } + + it 'extracts out the tag array correctly' do + expect(tags.length).to eq(4) + expect(described_class.tags).to include('entrypoint.workdir:app') + expect(described_class.tags).to include('entrypoint.name:irb') + expect(described_class.tags).to include('entrypoint.basedir:app') + expect(described_class.tags).to include('entrypoint.type:script') + end + end + + context 'with my/path/rubyapp.rb' do + include_context 'with mocked process environment' + let(:program_name) { 'my/path/rubyapp.rb' } + + it 'extracts out the tag array correctly' do + expect(tags.length).to eq(4) + expect(described_class.tags).to include('entrypoint.workdir:app') + expect(described_class.tags).to include('entrypoint.name:rubyapp.rb') + expect(described_class.tags).to include('entrypoint.basedir:path') + expect(described_class.tags).to include('entrypoint.type:script') + end + end + + context 'with my/path/foo:,bar' do + include_context 'with mocked process environment' + let(:program_name) { 'my/path/foo:,bar' } + + it 'extracts out the tag array correctly' do + expect(tags.length).to eq(4) + expect(described_class.tags).to include('entrypoint.workdir:app') + expect(described_class.tags).to include('entrypoint.name:foo_bar') + expect(described_class.tags).to include('entrypoint.basedir:path') + expect(described_class.tags).to include('entrypoint.type:script') + end + end + + context 'with bin/rails' do + include_context 'with mocked process environment' + let(:program_name) { 'bin/rails' } + + it 'extracts out the tags array correctly' do + expect(tags.length).to eq(4) + expect(described_class.tags).to include('entrypoint.workdir:app') + expect(described_class.tags).to include('entrypoint.name:rails') + expect(described_class.tags).to include('entrypoint.basedir:bin') + expect(described_class.tags).to include('entrypoint.type:script') + end + end + end end diff --git a/spec/datadog/core/remote/client_spec.rb b/spec/datadog/core/remote/client_spec.rb index 56f3c034bc2..6ba0f2515ce 100644 --- a/spec/datadog/core/remote/client_spec.rb +++ b/spec/datadog/core/remote/client_spec.rb @@ -677,6 +677,33 @@ end end + context 'process_tags' do + let(:client_payload) { client.send(:payload)[:client] } + + context 'when process tags propagation is enabled' do + include_context 'with mocked process environment' + before do + allow(Datadog.configuration).to receive(:experimental_propagate_process_tags_enabled).and_return(true) + end + + it 'has process tags in the payload' do + process_tags = client_payload[:client_tracer][:process_tags] + expect(process_tags).to be_a(Array) + expect(process_tags).to include('entrypoint.workdir:app') + expect(process_tags).to include('entrypoint.name:rspec') + expect(process_tags).to include('entrypoint.basedir:bin') + expect(process_tags).to include('entrypoint.type:script') + end + end + + context 'when process tags propagation is not enabled' do + # Currently false by default + it 'does not have process tags in the payload' do + expect(client_payload[:client_tracer]).not_to have_key(:process_tags) + end + end + end + context 'cached_target_files' do it 'returns cached_target_files' do state = repository.state diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bb377dec1f8..7e83ad4b6b4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,6 +36,7 @@ require 'support/network_helpers' require 'support/object_space_helper' require 'support/platform_helpers' +require 'support/process_helpers' require 'support/span_helpers' require 'support/spy_transport' require 'support/synchronization_helpers' diff --git a/spec/support/process_helpers.rb b/spec/support/process_helpers.rb new file mode 100644 index 00000000000..7858aa65d02 --- /dev/null +++ b/spec/support/process_helpers.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with mocked process environment' do + def reset_memoized_variables! + [:@serialized, :@tags].each do |variable| + Datadog::Core::Environment::Process.remove_instance_variable(variable) if + Datadog::Core::Environment::Process.instance_variable_defined?(variable) + end + end + + let(:program_name) { 'bin/rspec' } + let(:pwd) { '/app' } + + around do |example| + @original_0 = $0 + $0 = program_name + example.run + $0 = @original_0 + end + + before do + allow(Dir).to receive(:pwd).and_return(pwd) + allow(File).to receive(:expand_path).and_call_original + allow(File).to receive(:expand_path).with('.').and_return('/app') + reset_memoized_variables! + end + + after do + reset_memoized_variables! + end +end