Skip to content

Commit 1402a32

Browse files
authored
Merge pull request #3826 from DataDog/tonycthsu/crashtracking
Enable crashtracking without profiler
2 parents d2498fb + 85bd6b8 commit 1402a32

33 files changed

+959
-587
lines changed

.gitlab/install_datadog_deps.rb

+23-8
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,21 @@
6363

6464
puts gem_version_mapping
6565

66-
gem_version_mapping.each do |gem, version|
67-
env = {}
66+
env = {
67+
'GEM_HOME' => versioned_path.to_s,
68+
# Install `datadog` gem locally without its profiling native extension
69+
'DD_PROFILING_NO_EXTENSION' => 'true',
70+
}
71+
72+
[
73+
'debase-ruby_core_source',
74+
'ffi',
75+
'libddwaf',
76+
'msgpack',
77+
'libdatadog', # libdatadog MUST be installed before datadog to ensure libdatadog native extension is compiled
78+
'datadog',
79+
].each do |gem|
80+
version = gem_version_mapping.delete(gem)
6881

6982
gem_install_cmd = "gem install #{gem} "\
7083
"--version #{version} "\
@@ -73,19 +86,13 @@
7386

7487
case gem
7588
when 'ffi'
76-
gem_install_cmd << "--install-dir #{versioned_path} "
7789
# Install `ffi` gem with its built-in `libffi` native extension instead of using system's `libffi`
7890
gem_install_cmd << '-- --disable-system-libffi '
7991
when 'datadog'
80-
# Install `datadog` gem locally without its profiling native extension
81-
env['DD_PROFILING_NO_EXTENSION'] = 'true'
8292
gem_install_cmd =
8393
"gem install --local #{ENV.fetch('DATADOG_GEM_LOCATION')} "\
8494
'--no-document '\
8595
'--ignore-dependencies '\
86-
"--install-dir #{versioned_path} "
87-
else
88-
gem_install_cmd << "--install-dir #{versioned_path} "
8996
end
9097

9198
puts "Execute: #{gem_install_cmd}"
@@ -99,6 +106,14 @@
99106
end
100107
end
101108

109+
raise "#{gem_version_mapping.keys.join(',')} are not installed." if gem_version_mapping.any?
110+
111+
datadog_gem_path = versioned_path.join("gems/datadog-#{ENV.fetch('RUBY_PACKAGE_VERSION')}")
112+
libdatadog_so_file = "libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}.so"
113+
unless File.exist?("#{datadog_gem_path}/lib/#{libdatadog_so_file}")
114+
raise "Missing #{libdatadog_so_file} in #{datadog_gem_path}."
115+
end
116+
102117
FileUtils.cd(versioned_path.join("extensions/#{Gem::Platform.local}"), verbose: true) do
103118
# Symlink those directories to be utilized by Ruby compiled with shared libraries
104119
FileUtils.ln_sf Gem.extension_api_version, ruby_api_version

Matrixfile

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby',
88
'core-old' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby'
99
},
10+
'crashtracking' => {
11+
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ❌ jruby',
12+
},
1013
'appsec:main' => {
1114
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby'
1215
},

Rakefile

+11-2
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ namespace :spec do
7474
task all: [:main, :benchmark,
7575
:rails, :railsredis, :railsredis_activesupport, :railsactivejob,
7676
:elasticsearch, :http, :redis, :sidekiq, :sinatra, :hanami, :hanami_autoinstrument,
77-
:profiling]
77+
:profiling, :crashtracking]
7878

7979
desc '' # "Explicitly hiding from `rake -T`"
8080
RSpec::Core::RakeTask.new(:main) do |t, args|
8181
t.pattern = 'spec/**/*_spec.rb'
82-
t.exclude_pattern = 'spec/**/{contrib,benchmark,redis,auto_instrument,opentelemetry,profiling}/**/*_spec.rb,'\
82+
t.exclude_pattern = 'spec/**/{contrib,benchmark,redis,auto_instrument,opentelemetry,profiling,crashtracking}/**/*_spec.rb,'\
8383
' spec/**/{auto_instrument,opentelemetry}_spec.rb, spec/datadog/gem_packaging_spec.rb'
8484
t.rspec_opts = args.to_a.join(' ')
8585
end
@@ -170,6 +170,15 @@ namespace :spec do
170170
t.rspec_opts = args.to_a.join(' ')
171171
end
172172

173+
# rubocop:disable Style/MultilineBlockChain
174+
RSpec::Core::RakeTask.new(:crashtracking) do |t, args|
175+
t.pattern = 'spec/datadog/core/crashtracking/**/*_spec.rb'
176+
t.rspec_opts = args.to_a.join(' ')
177+
end.tap do |t|
178+
Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"])
179+
end
180+
# rubocop:enable Style/MultilineBlockChain
181+
173182
desc '' # "Explicitly hiding from `rake -T`"
174183
RSpec::Core::RakeTask.new(:contrib) do |t, args|
175184
contrib_paths = [

ext/libdatadog_api/crashtracker.c

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55

66
static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
77
static VALUE _native_stop(DDTRACE_UNUSED VALUE _self);
8-
static void crashtracker_init(VALUE profiling_module);
8+
static void crashtracker_init(VALUE crashtracking_module);
99

1010
// Used to report Ruby VM crashes.
1111
// Once initialized, segfaults will be reported automatically using libdatadog.
1212

1313
void DDTRACE_EXPORT Init_libdatadog_api(void) {
1414
VALUE datadog_module = rb_define_module("Datadog");
15-
VALUE profiling_module = rb_define_module_under(datadog_module, "Profiling");
15+
VALUE core_module = rb_define_module_under(datadog_module, "Core");
16+
VALUE crashtracking_module = rb_define_module_under(core_module, "Crashtracking");
1617

17-
crashtracker_init(profiling_module);
18+
crashtracker_init(crashtracking_module);
1819
}
1920

20-
void crashtracker_init(VALUE profiling_module) {
21-
VALUE crashtracker_class = rb_define_class_under(profiling_module, "Crashtracker", rb_cObject);
21+
void crashtracker_init(VALUE crashtracking_module) {
22+
VALUE crashtracker_class = rb_define_class_under(crashtracking_module, "Component", rb_cObject);
2223

2324
rb_define_singleton_method(crashtracker_class, "_native_start_or_update_on_fork", _native_start_or_update_on_fork, -1);
2425
rb_define_singleton_method(crashtracker_class, "_native_stop", _native_stop, 0);

lib/datadog/core/configuration/components.rb

+15-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require_relative '../../tracing/component'
1414
require_relative '../../profiling/component'
1515
require_relative '../../appsec/component'
16+
require_relative '../crashtracking/component'
1617

1718
module Datadog
1819
module Core
@@ -58,6 +59,17 @@ def build_runtime_metrics_worker(settings)
5859
def build_telemetry(settings, agent_settings, logger)
5960
Telemetry::Component.build(settings, agent_settings, logger)
6061
end
62+
63+
def build_crashtracker(settings, agent_settings, logger:)
64+
return unless settings.crashtracking.enabled
65+
66+
if (libdatadog_api_failure = Datadog::Core::Crashtracking::Component::LIBDATADOG_API_FAILURE)
67+
logger.debug("Cannot enable crashtracking: #{libdatadog_api_failure}")
68+
return
69+
end
70+
71+
Datadog::Core::Crashtracking::Component.build(settings, agent_settings, logger: logger)
72+
end
6173
end
6274

6375
include Datadog::Tracing::Component::InstanceMethods
@@ -70,6 +82,7 @@ def build_telemetry(settings, agent_settings, logger)
7082
:runtime_metrics,
7183
:telemetry,
7284
:tracer,
85+
:crashtracker,
7386
:appsec
7487

7588
def initialize(settings)
@@ -83,11 +96,12 @@ def initialize(settings)
8396

8497
@remote = Remote::Component.build(settings, agent_settings)
8598
@tracer = self.class.build_tracer(settings, agent_settings, logger: @logger)
99+
@crashtracker = self.class.build_crashtracker(settings, agent_settings, logger: @logger)
86100

87101
@profiler, profiler_logger_extra = Datadog::Profiling::Component.build_profiler_component(
88102
settings: settings,
89103
agent_settings: agent_settings,
90-
optional_tracer: @tracer,
104+
optional_tracer: @tracer
91105
)
92106
@environment_logger_extra.merge!(profiler_logger_extra) if profiler_logger_extra
93107

lib/datadog/core/configuration/settings.rb

+18-10
Original file line numberDiff line numberDiff line change
@@ -451,17 +451,16 @@ def initialize(*_)
451451
o.default 60
452452
end
453453

454-
# Enables reporting of information when the Ruby VM crashes.
455-
#
456-
# This feature is no longer experimental, and we plan to deprecate this setting and replace it with a
457-
# properly-named one soon.
458-
#
459-
# @default `DD_PROFILING_EXPERIMENTAL_CRASH_TRACKING_ENABLED` environment variable as a boolean,
460-
# otherwise `true`
454+
# DEV-3.0: Remove `experimental_crash_tracking_enabled` option
461455
option :experimental_crash_tracking_enabled do |o|
462-
o.type :bool
463-
o.env 'DD_PROFILING_EXPERIMENTAL_CRASH_TRACKING_ENABLED'
464-
o.default true
456+
o.after_set do |_, _, precedence|
457+
unless precedence == Datadog::Core::Configuration::Option::Precedence::DEFAULT
458+
Core.log_deprecation(key: :experimental_crash_tracking_enabled) do
459+
'The profiling.advanced.experimental_crash_tracking_enabled setting has been deprecated for removal '\
460+
'and no longer does anything. Please remove it from your Datadog.configure block.'
461+
end
462+
end
463+
end
465464
end
466465
end
467466

@@ -833,6 +832,15 @@ def initialize(*_)
833832
option :service
834833
end
835834

835+
settings :crashtracking do
836+
# Enables reporting of information when Ruby VM crashes.
837+
option :enabled do |o|
838+
o.type :bool
839+
o.default true
840+
o.env 'DD_CRASHTRACKING_ENABLED'
841+
end
842+
end
843+
836844
# TODO: Tracing should manage its own settings.
837845
# Keep this extension here for now to keep things working.
838846
extend Datadog::Tracing::Configuration::Settings
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../configuration/ext'
4+
5+
module Datadog
6+
module Core
7+
module Crashtracking
8+
# This module provides a method to resolve the base URL of the agent
9+
module AgentBaseUrl
10+
def self.resolve(agent_settings)
11+
case agent_settings.adapter
12+
when Datadog::Core::Configuration::Ext::Agent::HTTP::ADAPTER
13+
"#{agent_settings.ssl ? 'https' : 'http'}://#{agent_settings.hostname}:#{agent_settings.port}/"
14+
when Datadog::Core::Configuration::Ext::Agent::UnixSocket::ADAPTER
15+
"unix://#{agent_settings.uds_path}"
16+
end
17+
end
18+
end
19+
end
20+
end
21+
end
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# frozen_string_literal: true
2+
3+
require 'libdatadog'
4+
5+
require_relative 'tag_builder'
6+
require_relative 'agent_base_url'
7+
require_relative '../utils/only_once'
8+
require_relative '../utils/at_fork_monkey_patch'
9+
10+
module Datadog
11+
module Core
12+
module Crashtracking
13+
# Used to report Ruby VM crashes.
14+
#
15+
# NOTE: The crashtracker native state is a singleton;
16+
# so even if you create multiple instances of `Crashtracking::Component` and start them,
17+
# it only works as "last writer wins". Same for stop -- there's only one state, so calling stop
18+
# on it will stop the crash tracker, regardless of which instance started it.
19+
#
20+
# Methods prefixed with _native_ are implemented in `crashtracker.c`
21+
class Component
22+
LIBDATADOG_API_FAILURE =
23+
begin
24+
require "libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"
25+
nil
26+
rescue LoadError => e
27+
e.message
28+
end
29+
30+
ONLY_ONCE = Core::Utils::OnlyOnce.new
31+
32+
def self.build(settings, agent_settings, logger:)
33+
tags = TagBuilder.call(settings)
34+
agent_base_url = AgentBaseUrl.resolve(agent_settings)
35+
logger.warn('Missing agent base URL; cannot enable crash tracking') unless agent_base_url
36+
37+
ld_library_path = ::Libdatadog.ld_library_path
38+
logger.warn('Missing ld_library_path; cannot enable crash tracking') unless ld_library_path
39+
40+
path_to_crashtracking_receiver_binary = ::Libdatadog.path_to_crashtracking_receiver_binary
41+
unless path_to_crashtracking_receiver_binary
42+
logger.warn('Missing path_to_crashtracking_receiver_binary; cannot enable crash tracking')
43+
end
44+
45+
return unless agent_base_url
46+
return unless ld_library_path
47+
return unless path_to_crashtracking_receiver_binary
48+
49+
new(
50+
tags: tags,
51+
agent_base_url: agent_base_url,
52+
ld_library_path: ld_library_path,
53+
path_to_crashtracking_receiver_binary: path_to_crashtracking_receiver_binary,
54+
logger: logger
55+
).tap(&:start)
56+
end
57+
58+
def initialize(tags:, agent_base_url:, ld_library_path:, path_to_crashtracking_receiver_binary:, logger:)
59+
@tags = tags
60+
@agent_base_url = agent_base_url
61+
@ld_library_path = ld_library_path
62+
@path_to_crashtracking_receiver_binary = path_to_crashtracking_receiver_binary
63+
@logger = logger
64+
end
65+
66+
def start
67+
Utils::AtForkMonkeyPatch.apply!
68+
69+
start_or_update_on_fork(action: :start)
70+
ONLY_ONCE.run do
71+
Utils::AtForkMonkeyPatch.at_fork(:child) do
72+
# Must NOT reference `self` here, as only the first instance will
73+
# be captured by the ONLY_ONCE and we want to pick the latest active one
74+
# (which may have different tags or agent config)
75+
Datadog.send(:components).crashtracker&.update_on_fork
76+
end
77+
end
78+
end
79+
80+
def update_on_fork
81+
start_or_update_on_fork(action: :update_on_fork)
82+
end
83+
84+
def stop
85+
self.class._native_stop
86+
logger.debug('Crash tracking stopped successfully')
87+
rescue => e
88+
logger.error("Failed to stop crash tracking: #{e.message}")
89+
end
90+
91+
private
92+
93+
attr_reader :tags, :agent_base_url, :ld_library_path, :path_to_crashtracking_receiver_binary, :logger
94+
95+
def start_or_update_on_fork(action:)
96+
self.class._native_start_or_update_on_fork(
97+
action: action,
98+
exporter_configuration: [:agent, agent_base_url],
99+
path_to_crashtracking_receiver_binary: path_to_crashtracking_receiver_binary,
100+
ld_library_path: ld_library_path,
101+
tags_as_array: tags.to_a,
102+
upload_timeout_seconds: 1
103+
)
104+
logger.debug("Crash tracking #{action} successfully")
105+
rescue => e
106+
logger.error("Failed to #{action} crash tracking: #{e.message}")
107+
end
108+
end
109+
end
110+
end
111+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../utils'
4+
require_relative '../environment/socket'
5+
require_relative '../environment/identity'
6+
require_relative '../environment/git'
7+
8+
module Datadog
9+
module Core
10+
module Crashtracking
11+
# This module builds a hash of tags
12+
module TagBuilder
13+
def self.call(settings)
14+
hash = {
15+
'host' => Environment::Socket.hostname,
16+
'process_id' => Process.pid.to_s,
17+
'runtime_engine' => Environment::Identity.lang_engine,
18+
'runtime-id' => Environment::Identity.id,
19+
'runtime_platform' => Environment::Identity.lang_platform,
20+
'runtime_version' => Environment::Identity.lang_version,
21+
'env' => settings.env,
22+
'service' => settings.service,
23+
'version' => settings.version,
24+
'git.repository_url' => Environment::Git.git_repository_url,
25+
'git.commit.sha' => Environment::Git.git_commit_sha,
26+
'is_crash' => true
27+
}.compact
28+
29+
# Make sure everything is an utf-8 string, to avoid encoding issues in downstream
30+
settings.tags.merge(hash).each_with_object({}) do |(key, value), h|
31+
h[Utils.utf8_encode(key)] = Utils.utf8_encode(value)
32+
end
33+
end
34+
end
35+
end
36+
end
37+
end

lib/datadog/profiling.rb

-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ def self.allocation_count # rubocop:disable Lint/NestedMethodDefinition (On purp
144144
require_relative 'profiling/collectors/idle_sampling_helper'
145145
require_relative 'profiling/collectors/stack'
146146
require_relative 'profiling/collectors/thread_context'
147-
require_relative 'profiling/crashtracker'
148147
require_relative 'profiling/stack_recorder'
149148
require_relative 'profiling/exporter'
150149
require_relative 'profiling/flush'

0 commit comments

Comments
 (0)