diff --git a/lib/datadog/core/environment/process.rb b/lib/datadog/core/environment/process.rb index a5a9664bbf3..2c1b55b55e2 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_array.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_array + return @tags_array if defined?(@tags_array) 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_array = 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..f63e4653033 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_array + 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..5576b3716ac 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: Array[::String] def self.serialized: () -> ::String + def self.tags_array: () -> ::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..48ddf246feb 100644 --- a/spec/datadog/core/environment/process_spec.rb +++ b/spec/datadog/core/environment/process_spec.rb @@ -3,13 +3,14 @@ require 'open3' RSpec.describe Datadog::Core::Environment::Process do + def reset_memoized_variables! + described_class.remove_instance_variable(:@serialized) if described_class.instance_variable_defined?(:@serialized) + described_class.remove_instance_variable(:@tags_array) if described_class.instance_variable_defined?(:@tags_array) + end + 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' } @@ -24,11 +25,11 @@ def reset_serialized! 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! + reset_memoized_variables! end after do - reset_serialized! + reset_memoized_variables! end end @@ -147,4 +148,107 @@ def reset_serialized! end end end + describe '::tags_array' do + subject(:tags_array) { described_class.tags_array } + + 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_memoized_variables! + end + + after do + reset_memoized_variables! + end + end + + it { is_expected.to be_a_kind_of(Array) } + + it 'is an array of strings' do + expect(tags_array).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_array + second_call = described_class.tags_array + 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_array.length).to eq(4) + expect(described_class.tags_array).to include('entrypoint.workdir:app') + expect(described_class.tags_array).to include('entrypoint.name:executable') + expect(described_class.tags_array).to include('entrypoint.basedir:expectedbasedir') + expect(described_class.tags_array).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_array.length).to eq(4) + expect(described_class.tags_array).to include('entrypoint.workdir:app') + expect(described_class.tags_array).to include('entrypoint.name:irb') + expect(described_class.tags_array).to include('entrypoint.basedir:app') + expect(described_class.tags_array).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_array.length).to eq(4) + expect(described_class.tags_array).to include('entrypoint.workdir:app') + expect(described_class.tags_array).to include('entrypoint.name:rubyapp.rb') + expect(described_class.tags_array).to include('entrypoint.basedir:path') + expect(described_class.tags_array).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_array.length).to eq(4) + expect(described_class.tags_array).to include('entrypoint.workdir:app') + expect(described_class.tags_array).to include('entrypoint.name:foo_bar') + expect(described_class.tags_array).to include('entrypoint.basedir:path') + expect(described_class.tags_array).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_array.length).to eq(4) + expect(described_class.tags_array).to include('entrypoint.workdir:app') + expect(described_class.tags_array).to include('entrypoint.name:rails') + expect(described_class.tags_array).to include('entrypoint.basedir:bin') + expect(described_class.tags_array).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..159eb3adc00 100644 --- a/spec/datadog/core/remote/client_spec.rb +++ b/spec/datadog/core/remote/client_spec.rb @@ -677,6 +677,41 @@ end end + context 'process_tags' do + let(:client_payload) { client.send(:payload)[:client] } + + context 'when process tags propagation is enabled' do + before do + allow(Datadog.configuration).to receive(:experimental_propagate_process_tags_enabled).and_return(true) + if Datadog::Core::Environment::Process.instance_variable_defined?(:@tags_array) + Datadog::Core::Environment::Process.remove_instance_variable(:@tags_array) + end + 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 + # Current false by default + before do + if Datadog::Core::Environment::Process.instance_variable_defined?(:@tags_array) + Datadog::Core::Environment::Process.remove_instance_variable(:@tags_array) + end + end + + 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