Skip to content

add rails.application to process tags when available#5468

Open
wantsui wants to merge 9 commits intomasterfrom
add-application-name-rails-process-tags
Open

add rails.application to process tags when available#5468
wantsui wants to merge 9 commits intomasterfrom
add-application-name-rails-process-tags

Conversation

@wantsui
Copy link
Collaborator

@wantsui wantsui commented Mar 17, 2026

What does this PR do?

Adds the Rails application name to the process tags list to give Rails users a way to differentiate the application.

This PR also adds a "recompute" functionality in the process.rb because it turns out that Rails apps don't have access to their name until AFTER initialization. I found this out because I up a real Rails app and noticed the process tags with the default Rails utils was returning no name.

Now we'll recompute the process tags after Rails initialization, and here's an example of how it will appear in the Profiles under "Runtime Info":
image

Motivation:

This came about from an investigation with @marcotc and @raphaelgavache into the way the default Rails Dockerfile stores apps. (Anything using that base Dockerfile will store the app in the same app folder, so the existing process tags are not unique enough).

Change log entry

Yes. Add rails.application to the process tags.

Additional Notes:

This reuses the existing Rails utils and updates the process tags, but the utils had to be refactored. You may ask, "why"?

Reason 1: CI was failing on not being able to call underscore on a nil

Example: https://github.com/DataDog/dd-trace-rb/actions/runs/23263729755/job/67639506905?pr=5468

#14 2.338 rails aborted!
#14 2.339 NoMethodError: undefined method `underscore' for nil:NilClass
#14 2.339 /usr/local/bundle/gems/datadog-2.30.0/lib/datadog/core/contrib/rails/utils.rb:11:in `app_name'
#14 2.339 /usr/local/bundle/gems/datadog-2.30.0/lib/datadog/core/environment/process.rb:90:in `rails_application'
#14 2.339 /usr/local/bundle/gems/datadog-2.30.0/lib/datadog/core/environment/process.rb:39:in `tags'
#14 2.339 /usr/local/bundle/gems/datadog-2.30.0/lib/datadog/core/environment/process.rb:19:in `serialized'
#14 2.339 /usr/local/bundle/gems/datadog-2.30.0/lib/datadog/core/crashtracking/tag_builder.rb:18:in `call'
#14 2.339 /usr/local/bundle/gems/datadog-2.30.0/lib/datadog/core/crashtracking/component.rb:67:in `latest_tag

I updated the utils to skip trying to do call underscore if the parent namespace is missing.

Reason 2: But then I ran into an exception error because sometimes the CI doesn't have access to the app name yet?

To deal with this, some error handling was added and tests.

Example run: https://github.com/DataDog/dd-trace-rb/actions/runs/23265763535/job/67646150038?pr=5468


Failures:

  1) Datadog::AppSec::Contrib::Rails::Patcher.patch :after_routes_loaded hook when error occurs while getting application routes logs the error and reports it via telemetry
     Failure/Error: application_name = ::Rails.application.class.public_send(namespace_method)

     StandardError:
       StandardError
     # ./lib/datadog/core/contrib/rails/utils.rb:11:in `app_name'
     # ./lib/datadog/core/environment/process.rb:90:in `rails_application'
     # ./lib/datadog/core/environment/process.rb:39:in `tags'
     # ./lib/datadog/core/environment/process.rb:19:in `serialized'
     # ./lib/datadog/core/crashtracking/tag_builder.rb:18:in `call'
     # ./lib/datadog/core/crashtracking/component.rb:67:in `latest_tags'
     # ./lib/datadog/core/crashtracking/component.rb:20:in `build'
     # ./lib/datadog/core/configuration/components.rb:85:in `build_crashtracker'
     # ./lib/datadog/core/configuration/components.rb:153:in `initialize'
     # ./lib/datadog/core/configuration.rb:259:in `new'
     # ./lib/datadog/core/configuration.rb:259:in `build_components'
     # ./lib/datadog/core/configuration.rb:204:in `block in components'
     # ./lib/datadog/core/configuration.rb:238:in `block in safely_synchronize'
     # ./lib/datadog/core/configuration.rb:237:in `synchronize'
     # ./lib/datadog/core/configuration.rb:237:in `safely_synchronize'
     # ./lib/datadog/core/configuration.rb:200:in `components'
     # ./lib/datadog/tracing.rb:178:in `components'
     # ./lib/datadog/tracing.rb:182:in `tracer'
     # ./spec/datadog/tracing/contrib/support/tracer_helpers.rb:16:in `tracer'
     # ./spec/datadog/tracing/contrib/support/tracer_helpers.rb:102:in `block (2 levels) in <module:TracerHelpers>'
     # ./spec/datadog/tracing/contrib/support/tracer_helpers.rb:96:in `block (2 levels) in <module:TracerHelpers>'
     # ./spec/spec_helper.rb:274:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:154:in `block (2 levels) in <top (required)>'
     # /usr/local/bundle/gems/webmock-3.25.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # /usr/local/bundle/gems/rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'
     # ./spec/support/execute_in_fork.rb:32:in `run'

Finished in 2 minutes 44.9 seconds (files took 1.65 seconds to load)
204 examples, 1 failure

Failed examples:

rspec ./spec/datadog/appsec/contrib/rails/patcher_spec.rb:59 # Datadog::AppSec::Contrib::Rails::Patcher.patch :after_routes_loaded hook when error occurs while getting application routes logs the error and reports it via telemetry

Both errors showed that there were some Rails/CI specific ordering that prevent the Rails app name from being obtained.

How to test the change?

I tested with a Rails 8 that I sent data to the backend for.

@wantsui wantsui requested a review from marcotc March 17, 2026 22:06
@github-actions github-actions bot added the core Involves Datadog core libraries label Mar 17, 2026
@datadog-datadog-prod-us1
Copy link
Contributor

datadog-datadog-prod-us1 bot commented Mar 17, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 95.56%
Overall Coverage: 95.12% (-0.02%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c8049a0 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@wantsui wantsui added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Mar 18, 2026
@pr-commenter
Copy link

pr-commenter bot commented Mar 18, 2026

Benchmarks

Benchmark execution time: 2026-03-20 18:47:34

Comparing candidate commit c8049a0 in PR branch add-application-name-rails-process-tags with baseline commit 78f9609 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 46 metrics, 0 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

@github-actions github-actions bot added integrations Involves tracing integrations tracing labels Mar 19, 2026
@wantsui wantsui requested a review from marcotc March 19, 2026 23:20
@wantsui wantsui marked this pull request as ready for review March 20, 2026 13:07
@wantsui wantsui requested review from a team as code owners March 20, 2026 13:07
@wantsui wantsui requested a review from vpellan March 20, 2026 13:07
return nil if application_name.nil?
application_name.underscore
rescue
# Adds a failsafe during app boot, teardown, or test stubs where the application is not initialized and this check gets performed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it makes sense to show anything in logs about that or it's not that important?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add one because I couldn't think of any information here that would be useful for debugging.

Going by the failed run before I added the rescue: https://github.com/DataDog/dd-trace-rb/actions/runs/23265763535/job/67646150038?pr=5468, if I did log the exception, it would just list out the type of error but it wasn't specific enough to track down the root cause:

     StandardError:
       StandardError
     # ./lib/datadog/core/contrib/rails/utils.rb:11:in `app_name'
     # ./lib/datadog/core/environment/process.rb:90:in `rails_application'
     # ./lib/datadog/core/environment/process.rb:39:in `tags'
     # ./lib/datadog/core/environment/process.rb:19:in `serialized'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, if we care about app boot, and this code should be OK otherwise potential issues, we might want to show warning, that some bad things happen and something might not work as expected now. So my suggestion is more about customer resurfacing, instead of debugging purposes.

Totally up to you!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point, I'll think of a useful message to add here!

Comment on lines +39 to +40
rails_application_name = TagNormalizer.normalize_process_value(rails_application.to_s)
tags << "#{Environment::Ext::TAG_RAILS_APPLICATION}:#{rails_application_name}" unless rails_application_name.empty?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Core namespace suppose to be free of contribs? I have a strong feelings that it doesn't belong here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: It feels like the Rails lookup might sit more naturally in the patcher, which already owns all the Rails-specific knowledge — leaving Process free of the Core::Contrib::Rails dependency entirely.

What if we gave Process a simple setter instead?

def self.rails_application_name=(name)
  @rails_application_name = name
  remove_instance_variable(:@tags) if instance_variable_defined?(:@tags)
  remove_instance_variable(:@serialized) if instance_variable_defined?(:@serialized)
end

Then in tags, swap the rails_application call for a direct check on @rails_application_name. The patcher would own the lookup:

if Datadog.configuration.experimental_propagate_process_tags_enabled
  Datadog::Core::Environment::Process.rails_application_name = Core::Contrib::Rails::Utils.app_name
end

No require in process.rb, no private method — and recompute_tags! goes away too since it was only introduced for this use case. Curious what you think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion and I'll try refactoring this way in a separate commit so it's easy to revert back if there are different suggestions to achieve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, we have lib/datadog/core/contrib/rails/utils.rb sitting in core today.
Do you guys think we should move this out of core too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just saw the Process#recompute_tags! method, and it's unfortunate that we have to have it to make it work, given the Rails of late initialization.

I agree then that instead of having the recompute tags, we should just set the application name in the sites where we call recompute tags today.

Here's my concern: we actually forgot to add Process#recompute_tags! to AppSec, since you can have AppSec on Rails without Tracing.
More importantly, if you only have Profiling enabled, the process tag rails_application_name should still be present, but they will never have an opportunity to be set. We'll have the same issue with DI only apps.

I think we probably have to subscribe to the Rails initializer just for core features, which for now would be this one (aka have an official Core::Contrib::Rails).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the point is a weak coupling, in this class nothing said that it should be aware about Rails.

Also without Rails that functionality is a dead-code. IMHO what Oleg suggests bring some loose coupling, but I would put it even further and remove knowledge of the framework at all via simple renaming

def self.application_name=(name)
  @application_name = name
  remove_instance_variable(:@tags) if instance_variable_defined?(:@tags)
  remove_instance_variable(:@serialized) if instance_variable_defined?(:@serialized)
end

Comment on lines +18 to +20
rails_version = Module.new
rails_version.const_set(:MAJOR, rails_version_major)
rails_module.const_set(:VERSION, rails_version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT if normal Ruby-way of defining module with constants?

Module.new do
  # ....
end

Copy link
Member

@p-datadog p-datadog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work overall.

That said — it feels like process.rb picking up a Rails dependency might work against the grain of Core. Right now Process is entirely framework-agnostic (just $0, Dir.pwd, File), and the require of Core::Contrib::Rails is the first thing to break that. Left a thought inline.

Comment on lines +39 to +40
rails_application_name = TagNormalizer.normalize_process_value(rails_application.to_s)
tags << "#{Environment::Ext::TAG_RAILS_APPLICATION}:#{rails_application_name}" unless rails_application_name.empty?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: It feels like the Rails lookup might sit more naturally in the patcher, which already owns all the Rails-specific knowledge — leaving Process free of the Core::Contrib::Rails dependency entirely.

What if we gave Process a simple setter instead?

def self.rails_application_name=(name)
  @rails_application_name = name
  remove_instance_variable(:@tags) if instance_variable_defined?(:@tags)
  remove_instance_variable(:@serialized) if instance_variable_defined?(:@serialized)
end

Then in tags, swap the rails_application call for a direct check on @rails_application_name. The patcher would own the lookup:

if Datadog.configuration.experimental_propagate_process_tags_enabled
  Datadog::Core::Environment::Process.rails_application_name = Core::Contrib::Rails::Utils.app_name
end

No require in process.rb, no private method — and recompute_tags! goes away too since it was only introduced for this use case. Curious what you think.

Comment on lines +10 to +11
namespace_method = (::Rails::VERSION::MAJOR >= 6) ? :module_parent_name : :parent_name
application_name = ::Rails.application.class.public_send(namespace_method)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid using public_send, because it's slower a direct method call, and in this case, we know the method name ahead of time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos core Involves Datadog core libraries integrations Involves tracing integrations tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants