datadog 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -1
  3. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
  4. data/ext/datadog_profiling_loader/extconf.rb +10 -22
  5. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +148 -30
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +4 -2
  7. data/ext/datadog_profiling_native_extension/collectors_stack.c +89 -46
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +580 -29
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +9 -1
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +0 -27
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -4
  12. data/ext/datadog_profiling_native_extension/extconf.rb +38 -21
  13. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  14. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  15. data/ext/datadog_profiling_native_extension/heap_recorder.c +20 -6
  16. data/ext/datadog_profiling_native_extension/http_transport.c +38 -6
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +52 -1
  18. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -0
  19. data/ext/datadog_profiling_native_extension/profiling.c +1 -1
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  21. data/ext/libdatadog_api/crashtracker.c +20 -18
  22. data/ext/libdatadog_api/datadog_ruby_common.c +0 -27
  23. data/ext/libdatadog_api/datadog_ruby_common.h +0 -4
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  26. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  27. data/lib/datadog/appsec/component.rb +29 -8
  28. data/lib/datadog/appsec/configuration/settings.rb +2 -2
  29. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  30. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  31. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  32. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +0 -14
  33. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +67 -31
  34. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +18 -15
  35. data/lib/datadog/appsec/contrib/graphql/integration.rb +14 -1
  36. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +2 -5
  37. data/lib/datadog/appsec/event.rb +1 -1
  38. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  39. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  40. data/lib/datadog/appsec/processor.rb +36 -37
  41. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  42. data/lib/datadog/appsec/remote.rb +7 -3
  43. data/lib/datadog/appsec.rb +2 -2
  44. data/lib/datadog/core/configuration/components.rb +4 -3
  45. data/lib/datadog/core/configuration/settings.rb +84 -5
  46. data/lib/datadog/core/crashtracking/component.rb +1 -1
  47. data/lib/datadog/core/environment/execution.rb +5 -5
  48. data/lib/datadog/core/metrics/client.rb +7 -0
  49. data/lib/datadog/core/rate_limiter.rb +183 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  51. data/lib/datadog/core/remote/component.rb +4 -2
  52. data/lib/datadog/core/remote/negotiation.rb +4 -4
  53. data/lib/datadog/core/remote/tie.rb +2 -0
  54. data/lib/datadog/core/runtime/metrics.rb +1 -1
  55. data/lib/datadog/core/telemetry/component.rb +2 -0
  56. data/lib/datadog/core/telemetry/event.rb +12 -7
  57. data/lib/datadog/core/telemetry/logger.rb +51 -0
  58. data/lib/datadog/core/telemetry/logging.rb +50 -14
  59. data/lib/datadog/core/telemetry/request.rb +13 -1
  60. data/lib/datadog/core/utils/time.rb +12 -0
  61. data/lib/datadog/di/code_tracker.rb +168 -0
  62. data/lib/datadog/di/configuration/settings.rb +163 -0
  63. data/lib/datadog/di/configuration.rb +11 -0
  64. data/lib/datadog/di/error.rb +31 -0
  65. data/lib/datadog/di/extensions.rb +16 -0
  66. data/lib/datadog/di/probe.rb +133 -0
  67. data/lib/datadog/di/probe_builder.rb +41 -0
  68. data/lib/datadog/di/redactor.rb +188 -0
  69. data/lib/datadog/di/serializer.rb +193 -0
  70. data/lib/datadog/di.rb +14 -0
  71. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  72. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +12 -10
  73. data/lib/datadog/profiling/collectors/info.rb +12 -3
  74. data/lib/datadog/profiling/collectors/thread_context.rb +26 -0
  75. data/lib/datadog/profiling/component.rb +20 -4
  76. data/lib/datadog/profiling/http_transport.rb +6 -1
  77. data/lib/datadog/profiling/scheduler.rb +2 -0
  78. data/lib/datadog/profiling/stack_recorder.rb +3 -0
  79. data/lib/datadog/single_step_instrument.rb +12 -0
  80. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  81. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  82. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  83. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  84. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  85. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  86. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  87. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +3 -1
  88. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  89. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  90. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  91. data/lib/datadog/tracing/contrib/faraday/middleware.rb +9 -0
  92. data/lib/datadog/tracing/contrib/grape/endpoint.rb +19 -0
  93. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  94. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  95. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  96. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +13 -9
  97. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +6 -3
  98. data/lib/datadog/tracing/contrib/http/instrumentation.rb +18 -15
  99. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +6 -5
  100. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  101. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +5 -0
  102. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  103. data/lib/datadog/tracing/contrib/lograge/patcher.rb +1 -2
  104. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  105. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  106. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  107. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  108. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  109. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  110. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  111. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  112. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  113. data/lib/datadog/tracing/metadata/ext.rb +2 -0
  114. data/lib/datadog/tracing/remote.rb +5 -2
  115. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  116. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  117. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  118. data/lib/datadog/tracing/sampling/rule_sampler.rb +9 -5
  119. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  120. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  121. data/lib/datadog/tracing/trace_operation.rb +26 -2
  122. data/lib/datadog/tracing/tracer.rb +14 -12
  123. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  124. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  125. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  126. data/lib/datadog/tracing/workers.rb +1 -1
  127. data/lib/datadog/version.rb +1 -1
  128. metadata +25 -8
  129. data/lib/datadog/tracing/sampling/rate_limiter.rb +0 -185
@@ -3,6 +3,8 @@
3
3
  #include <ruby.h>
4
4
  #include <stdbool.h>
5
5
 
6
+ #include "gvl_profiling_helper.h"
7
+
6
8
  void thread_context_collector_sample(
7
9
  VALUE self_instance,
8
10
  long current_monotonic_wall_time_ns,
@@ -12,5 +14,11 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
12
14
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
13
15
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance);
14
16
  void thread_context_collector_on_gc_start(VALUE self_instance);
15
- bool thread_context_collector_on_gc_finish(VALUE self_instance);
17
+ __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance);
16
18
  VALUE enforce_thread_context_collector_instance(VALUE object);
19
+
20
+ #ifndef NO_GVL_INSTRUMENTATION
21
+ void thread_context_collector_on_gvl_waiting(gvl_profiling_thread thread);
22
+ __attribute__((warn_unused_result)) bool thread_context_collector_on_gvl_running(gvl_profiling_thread thread);
23
+ VALUE thread_context_collector_sample_after_gvl_running(VALUE self_instance);
24
+ #endif
@@ -28,33 +28,6 @@ VALUE datadog_gem_version(void) {
28
28
  return version_string;
29
29
  }
30
30
 
31
- __attribute__((warn_unused_result))
32
- ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration) {
33
- ENFORCE_TYPE(exporter_configuration, T_ARRAY);
34
-
35
- VALUE exporter_working_mode = rb_ary_entry(exporter_configuration, 0);
36
- ENFORCE_TYPE(exporter_working_mode, T_SYMBOL);
37
- ID working_mode = SYM2ID(exporter_working_mode);
38
-
39
- ID agentless_id = rb_intern("agentless");
40
- ID agent_id = rb_intern("agent");
41
-
42
- if (working_mode != agentless_id && working_mode != agent_id) {
43
- rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
44
- }
45
-
46
- if (working_mode == agentless_id) {
47
- VALUE site = rb_ary_entry(exporter_configuration, 1);
48
- VALUE api_key = rb_ary_entry(exporter_configuration, 2);
49
-
50
- return ddog_prof_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
51
- } else { // agent_id
52
- VALUE base_url = rb_ary_entry(exporter_configuration, 1);
53
-
54
- return ddog_prof_Endpoint_agent(char_slice_from_ruby_string(base_url));
55
- }
56
- }
57
-
58
31
  static VALUE log_failure_to_process_tag(VALUE err_details) {
59
32
  VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
60
33
  VALUE logger = rb_funcall(datadog_module, rb_intern("logger"), 0);
@@ -27,7 +27,6 @@
27
27
  #define ENFORCE_BOOLEAN(value) \
28
28
  { if (RB_UNLIKELY(value != Qtrue && value != Qfalse)) raise_unexpected_type(value, ADD_QUOTES(value), "true or false", __FILE__, __LINE__, __func__); }
29
29
 
30
- // Called by ENFORCE_TYPE; should not be used directly
31
30
  NORETURN(void raise_unexpected_type(VALUE value, const char *value_name, const char *type_name, const char *file, int line, const char* function_name));
32
31
 
33
32
  // Helper to retrieve Datadog::VERSION::STRING
@@ -39,9 +38,6 @@ static inline ddog_CharSlice char_slice_from_ruby_string(VALUE string) {
39
38
  return char_slice;
40
39
  }
41
40
 
42
- __attribute__((warn_unused_result))
43
- ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration);
44
-
45
41
  __attribute__((warn_unused_result))
46
42
  ddog_Vec_Tag convert_tags(VALUE tags_as_array);
47
43
 
@@ -74,35 +74,25 @@ Logging.message("[datadog] Using compiler:\n")
74
74
  xsystem("#{CONFIG["CC"]} -v")
75
75
  Logging.message("[datadog] End of compiler information\n")
76
76
 
77
- # mkmf on modern Rubies actually has an append_cflags that does something similar
78
- # (see https://github.com/ruby/ruby/pull/5760), but as usual we need a bit more boilerplate to deal with legacy Rubies
79
- def add_compiler_flag(flag)
80
- if try_cflags(flag)
81
- $CFLAGS << " " << flag
82
- else
83
- $stderr.puts("WARNING: '#{flag}' not accepted by compiler, skipping it")
84
- end
85
- end
86
-
87
77
  # Because we can't control what compiler versions our customers use, shipping with -Werror by default is a no-go.
88
78
  # But we can enable it in CI, so that we quickly spot any new warnings that just got introduced.
89
- add_compiler_flag "-Werror" if ENV["DATADOG_GEM_CI"] == "true"
79
+ append_cflags "-Werror" if ENV["DATADOG_GEM_CI"] == "true"
90
80
 
91
81
  # Older gcc releases may not default to C99 and we need to ask for this. This is also used:
92
82
  # * by upstream Ruby -- search for gnu99 in the codebase
93
83
  # * by msgpack, another datadog gem dependency
94
84
  # (https://github.com/msgpack/msgpack-ruby/blob/18ce08f6d612fe973843c366ac9a0b74c4e50599/ext/msgpack/extconf.rb#L8)
95
- add_compiler_flag "-std=gnu99"
85
+ append_cflags "-std=gnu99"
96
86
 
97
87
  # Gets really noisy when we include the MJIT header, let's omit it (TODO: Use #pragma GCC diagnostic instead?)
98
- add_compiler_flag "-Wno-unused-function"
88
+ append_cflags "-Wno-unused-function"
99
89
 
100
90
  # Allow defining variables at any point in a function
101
- add_compiler_flag "-Wno-declaration-after-statement"
91
+ append_cflags "-Wno-declaration-after-statement"
102
92
 
103
93
  # If we forget to include a Ruby header, the function call may still appear to work, but then
104
94
  # cause a segfault later. Let's ensure that never happens.
105
- add_compiler_flag "-Werror-implicit-function-declaration"
95
+ append_cflags "-Werror-implicit-function-declaration"
106
96
 
107
97
  # The native extension is not intended to expose any symbols/functions for other native libraries to use;
108
98
  # the sole exception being `Init_datadog_profiling_native_extension` which needs to be visible for Ruby to call it when
@@ -110,14 +100,14 @@ add_compiler_flag "-Werror-implicit-function-declaration"
110
100
  #
111
101
  # By setting this compiler flag, we tell it to assume that everything is private unless explicitly stated.
112
102
  # For more details see https://gcc.gnu.org/wiki/Visibility
113
- add_compiler_flag "-fvisibility=hidden"
103
+ append_cflags "-fvisibility=hidden"
114
104
 
115
105
  # Avoid legacy C definitions
116
- add_compiler_flag "-Wold-style-definition"
106
+ append_cflags "-Wold-style-definition"
117
107
 
118
108
  # Enable all other compiler warnings
119
- add_compiler_flag "-Wall"
120
- add_compiler_flag "-Wextra"
109
+ append_cflags "-Wall"
110
+ append_cflags "-Wextra"
121
111
 
122
112
  if ENV["DDTRACE_DEBUG"] == "true"
123
113
  $defs << "-DDD_DEBUG"
@@ -153,6 +143,12 @@ $defs << "-DNO_RACTOR_HEADER_INCLUDE" if RUBY_VERSION < "3.3"
153
143
  # On older Rubies, some of the Ractor internal APIs were directly accessible
154
144
  $defs << "-DUSE_RACTOR_INTERNAL_APIS_DIRECTLY" if RUBY_VERSION < "3.3"
155
145
 
146
+ # On older Rubies, there was no GVL instrumentation API and APIs created to support it
147
+ $defs << "-DNO_GVL_INSTRUMENTATION" if RUBY_VERSION < "3.2"
148
+
149
+ # Supporting GVL instrumentation on 3.2 needs some workarounds
150
+ $defs << "-DUSE_GVL_PROFILING_3_2_WORKAROUNDS" if RUBY_VERSION.start_with?("3.2")
151
+
156
152
  # On older Rubies, there was no struct rb_native_thread. See private_vm_api_acccess.c for details.
157
153
  $defs << "-DNO_RB_NATIVE_THREAD" if RUBY_VERSION < "3.2"
158
154
 
@@ -255,7 +251,7 @@ if Datadog::Profiling::NativeExtensionHelpers::CAN_USE_MJIT_HEADER
255
251
 
256
252
  # Warn on unused parameters to functions. Use `DDTRACE_UNUSED` to mark things as known-to-not-be-used.
257
253
  # See the comment on the same flag below for why this is done last.
258
- add_compiler_flag "-Wunused-parameter"
254
+ append_cflags "-Wunused-parameter"
259
255
 
260
256
  create_makefile EXTENSION_NAME
261
257
  else
@@ -269,6 +265,27 @@ else
269
265
  require "debase/ruby_core_source"
270
266
  dir_config("ruby") # allow user to pass in non-standard core include directory
271
267
 
268
+ # This is a workaround for a weird issue...
269
+ #
270
+ # The mkmf tool defines a `with_cppflags` helper that debase-ruby_core_source uses. This helper temporarily
271
+ # replaces `$CPPFLAGS` (aka the C pre-processor [not c++!] flags) with a different set when doing something.
272
+ #
273
+ # The debase-ruby_core_source gem uses `with_cppflags` during makefile generation to inject extra headers into the
274
+ # path. But because `with_cppflags` replaces `$CPPFLAGS`, well, the default `$CPPFLAGS` are not included in the
275
+ # makefile.
276
+ #
277
+ # This is a problem because the default `$CPPFLAGS` carries configuration that was set when Ruby was being built.
278
+ # Thus, if we ignore it, we don't compile the profiler with the exact same configuration as Ruby.
279
+ # In practice, this can generate crashes and weird bugs if the Ruby configuration is tweaked in a manner that
280
+ # changes some of the internal structures that the profiler relies on. Concretely, setting for instance
281
+ # `VM_CHECK_MODE=1` when building Ruby will trigger this issue (because somethings in structures the profiler reads
282
+ # are ifdef'd out using this setting).
283
+ #
284
+ # To workaround this issue, we override `with_cppflags` for debase-ruby_core_source to still include `$CPPFLAGS`.
285
+ Debase::RubyCoreSource.define_singleton_method(:with_cppflags) do |newflags, &block|
286
+ super("#{newflags} #{$CPPFLAGS}", &block)
287
+ end
288
+
272
289
  Debase::RubyCoreSource
273
290
  .create_makefile_with_core(
274
291
  proc do
@@ -282,7 +299,7 @@ else
282
299
  # This is added as late as possible because in some Rubies we support (e.g. 3.3), adding this flag before
283
300
  # checking if internal VM headers are available causes those checks to fail because of this warning (and not
284
301
  # because the headers are not available.)
285
- add_compiler_flag "-Wunused-parameter"
302
+ append_cflags "-Wunused-parameter"
286
303
  end
287
304
 
288
305
  headers_available
@@ -0,0 +1,50 @@
1
+ #include <ruby.h>
2
+ #include <ruby/thread.h>
3
+ #include "gvl_profiling_helper.h"
4
+
5
+ #if !defined(NO_GVL_INSTRUMENTATION) && !defined(USE_GVL_PROFILING_3_2_WORKAROUNDS) // Ruby 3.3+
6
+ rb_internal_thread_specific_key_t gvl_waiting_tls_key;
7
+
8
+ void gvl_profiling_init(void) {
9
+ gvl_waiting_tls_key = rb_internal_thread_specific_key_create();
10
+ }
11
+
12
+ #endif
13
+
14
+ #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS // Ruby 3.2
15
+ __thread gvl_profiling_thread gvl_waiting_tls;
16
+ static bool gvl_profiling_state_thread_tracking_workaround_installed = false;
17
+
18
+ static void on_thread_start(
19
+ DDTRACE_UNUSED rb_event_flag_t _unused1,
20
+ DDTRACE_UNUSED const rb_internal_thread_event_data_t *_unused2,
21
+ DDTRACE_UNUSED void *_unused3
22
+ ) {
23
+ gvl_waiting_tls = (gvl_profiling_thread) {.thread = NULL};
24
+ }
25
+
26
+ // Hack: We're using the gvl_waiting_tls native thread-local to store per-thread information. Unfortunately, Ruby puts a big hole
27
+ // in our plan because it reuses native threads -- specifically, in Ruby 3.2, native threads are still 1:1 to Ruby
28
+ // threads (M:N wasn't a thing yet) BUT once a Ruby thread dies, the VM will keep the native thread around for a
29
+ // bit, and if another Ruby thread starts right after, Ruby will reuse the native thread, rather than create a new one.
30
+ //
31
+ // This will mean that the new Ruby thread will still have the same native thread-local data that we set on the
32
+ // old thread. For the purposes of our tracking, where we're keeping a pointer to the current thread object in
33
+ // thread-local storage **this is disastrous** since it means we'll be pointing at the wrong thread (and its
34
+ // memory may have been freed or reused since!)
35
+ //
36
+ // To work around this issue, once GVL profiling is enabled, we install an event hook on thread start
37
+ // events that clears the thread-local data. This guarantees that there will be no stale data -- any existing
38
+ // data will be cleared at thread start.
39
+ //
40
+ // Note that once installed, this event hook becomes permanent -- stopping the profiler does not stop this event
41
+ // hook, unlike all others. This is because we can't afford to miss any thread start events while the
42
+ // profiler is stopped (e.g. during reconfiguration) as that would mean stale data once the profiler starts again.
43
+ void gvl_profiling_state_thread_tracking_workaround(void) {
44
+ if (gvl_profiling_state_thread_tracking_workaround_installed) return;
45
+
46
+ rb_internal_thread_add_event_hook(on_thread_start, RUBY_INTERNAL_THREAD_EVENT_STARTED, NULL);
47
+
48
+ gvl_profiling_state_thread_tracking_workaround_installed = true;
49
+ }
50
+ #endif
@@ -0,0 +1,75 @@
1
+ #pragma once
2
+
3
+ // This helper is used by the Datadog::Profiling::Collectors::ThreadContext to store data used when profiling the GVL.
4
+ // It's tested through that class' interfaces.
5
+ // ---
6
+
7
+ #include "extconf.h"
8
+
9
+ #if !defined(NO_GVL_INSTRUMENTATION) && !defined(USE_GVL_PROFILING_3_2_WORKAROUNDS) // Ruby 3.3+
10
+ #include <ruby.h>
11
+ #include <ruby/thread.h>
12
+ #include "datadog_ruby_common.h"
13
+
14
+ typedef struct { VALUE thread; } gvl_profiling_thread;
15
+ extern rb_internal_thread_specific_key_t gvl_waiting_tls_key;
16
+
17
+ void gvl_profiling_init(void);
18
+
19
+ static inline gvl_profiling_thread thread_from_thread_object(VALUE thread) {
20
+ return (gvl_profiling_thread) {.thread = thread};
21
+ }
22
+
23
+ static inline gvl_profiling_thread thread_from_event(const rb_internal_thread_event_data_t *event_data) {
24
+ return thread_from_thread_object(event_data->thread);
25
+ }
26
+
27
+ static inline intptr_t gvl_profiling_state_get(gvl_profiling_thread thread) {
28
+ return (intptr_t) rb_internal_thread_specific_get(thread.thread, gvl_waiting_tls_key);
29
+ }
30
+
31
+ static inline void gvl_profiling_state_set(gvl_profiling_thread thread, intptr_t value) {
32
+ rb_internal_thread_specific_set(thread.thread, gvl_waiting_tls_key, (void *) value);
33
+ }
34
+ #endif
35
+
36
+ #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS // Ruby 3.2
37
+ typedef struct { void *thread; } gvl_profiling_thread;
38
+ extern __thread gvl_profiling_thread gvl_waiting_tls;
39
+
40
+ static inline void gvl_profiling_init(void) { }
41
+
42
+ // This header gets included in private_vm_access.c which can't include datadog_ruby_common.h so we replicate this
43
+ // helper here
44
+ #ifdef __GNUC__
45
+ #define DDTRACE_UNUSED __attribute__((unused))
46
+ #else
47
+ #define DDTRACE_UNUSED
48
+ #endif
49
+
50
+ // NOTE: This is a hack that relies on the knowledge that on Ruby 3.2 the
51
+ // RUBY_INTERNAL_THREAD_EVENT_READY and RUBY_INTERNAL_THREAD_EVENT_RESUMED events always get called on the thread they
52
+ // are about. Thus, we can use our thread local storage hack to get this data, even though the event doesn't include it.
53
+ static inline gvl_profiling_thread thread_from_event(DDTRACE_UNUSED const void *event_data) {
54
+ return gvl_waiting_tls;
55
+ }
56
+
57
+ void gvl_profiling_state_thread_tracking_workaround(void);
58
+ gvl_profiling_thread gvl_profiling_state_maybe_initialize(void);
59
+
60
+ // Implementing these on Ruby 3.2 requires access to private VM things, so the following methods are
61
+ // implemented in `private_vm_api_access.c`
62
+ gvl_profiling_thread thread_from_thread_object(VALUE thread);
63
+ intptr_t gvl_profiling_state_get(gvl_profiling_thread thread);
64
+ void gvl_profiling_state_set(gvl_profiling_thread thread, intptr_t value);
65
+ #endif
66
+
67
+ #ifndef NO_GVL_INSTRUMENTATION // For all Rubies supporting GVL profiling (3.2+)
68
+ static inline intptr_t gvl_profiling_state_thread_object_get(VALUE thread) {
69
+ return gvl_profiling_state_get(thread_from_thread_object(thread));
70
+ }
71
+
72
+ static inline void gvl_profiling_state_thread_object_set(VALUE thread, intptr_t value) {
73
+ gvl_profiling_state_set(thread_from_thread_object(thread), value);
74
+ }
75
+ #endif
@@ -632,12 +632,14 @@ static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t val
632
632
  ddog_prof_Location *locations = recorder->reusable_locations;
633
633
  for (uint16_t i = 0; i < stack->frames_len; i++) {
634
634
  const heap_frame *frame = &stack->frames[i];
635
- ddog_prof_Location *location = &locations[i];
636
- location->function.name.ptr = frame->name;
637
- location->function.name.len = strlen(frame->name);
638
- location->function.filename.ptr = frame->filename;
639
- location->function.filename.len = strlen(frame->filename);
640
- location->line = frame->line;
635
+ locations[i] = (ddog_prof_Location) {
636
+ .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C("")},
637
+ .function = {
638
+ .name = {.ptr = frame->name, .len = strlen(frame->name)},
639
+ .filename = {.ptr = frame->filename, .len = strlen(frame->filename)},
640
+ },
641
+ .line = frame->line,
642
+ };
641
643
  }
642
644
 
643
645
  heap_recorder_iteration_data iteration_data;
@@ -782,8 +784,20 @@ static void cleanup_heap_record_if_unused(heap_recorder *heap_recorder, heap_rec
782
784
  }
783
785
 
784
786
  static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, object_record *record) {
787
+ // @ivoanjo: We've seen a segfault crash in the field in this function (October 2024) which we're still trying to investigate.
788
+ // (See PROF-10656 Datadog-internal for details). Just in case, I've sprinkled a bunch of NULL tests in this function for now.
789
+ // Once we figure out the issue we can get rid of them again.
790
+
791
+ if (heap_recorder == NULL) rb_raise(rb_eRuntimeError, "heap_recorder was NULL in on_committed_object_record_cleanup");
792
+ if (heap_recorder->heap_records == NULL) rb_raise(rb_eRuntimeError, "heap_recorder->heap_records was NULL in on_committed_object_record_cleanup");
793
+ if (record == NULL) rb_raise(rb_eRuntimeError, "record was NULL in on_committed_object_record_cleanup");
794
+
785
795
  // Starting with the associated heap record. There will now be one less tracked object pointing to it
786
796
  heap_record *heap_record = record->heap_record;
797
+
798
+ if (heap_record == NULL) rb_raise(rb_eRuntimeError, "heap_record was NULL in on_committed_object_record_cleanup");
799
+ if (heap_record->stack == NULL) rb_raise(rb_eRuntimeError, "heap_record->stack was NULL in on_committed_object_record_cleanup");
800
+
787
801
  heap_record->num_tracked_objects--;
788
802
 
789
803
  // One less object using this heap record, it may have become unused...
@@ -77,6 +77,32 @@ static VALUE _native_validate_exporter(DDTRACE_UNUSED VALUE _self, VALUE exporte
77
77
  return rb_ary_new_from_args(2, ok_symbol, Qnil);
78
78
  }
79
79
 
80
+ static ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration) {
81
+ ENFORCE_TYPE(exporter_configuration, T_ARRAY);
82
+
83
+ VALUE exporter_working_mode = rb_ary_entry(exporter_configuration, 0);
84
+ ENFORCE_TYPE(exporter_working_mode, T_SYMBOL);
85
+ ID working_mode = SYM2ID(exporter_working_mode);
86
+
87
+ ID agentless_id = rb_intern("agentless");
88
+ ID agent_id = rb_intern("agent");
89
+
90
+ if (working_mode != agentless_id && working_mode != agent_id) {
91
+ rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
92
+ }
93
+
94
+ if (working_mode == agentless_id) {
95
+ VALUE site = rb_ary_entry(exporter_configuration, 1);
96
+ VALUE api_key = rb_ary_entry(exporter_configuration, 2);
97
+
98
+ return ddog_prof_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
99
+ } else { // agent_id
100
+ VALUE base_url = rb_ary_entry(exporter_configuration, 1);
101
+
102
+ return ddog_prof_Endpoint_agent(char_slice_from_ruby_string(base_url));
103
+ }
104
+ }
105
+
80
106
  static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration, VALUE tags_as_array) {
81
107
  ENFORCE_TYPE(exporter_configuration, T_ARRAY);
82
108
  ENFORCE_TYPE(tags_as_array, T_ARRAY);
@@ -115,8 +141,7 @@ static VALUE perform_export(
115
141
  ddog_prof_Exporter_Slice_File files_to_export_unmodified,
116
142
  ddog_Vec_Tag *additional_tags,
117
143
  ddog_CharSlice internal_metadata,
118
- ddog_CharSlice info,
119
- uint64_t timeout_milliseconds
144
+ ddog_CharSlice info
120
145
  ) {
121
146
  ddog_prof_ProfiledEndpointsStats *endpoints_stats = NULL; // Not in use yet
122
147
  ddog_prof_Exporter_Request_BuildResult build_result = ddog_prof_Exporter_Request_build(
@@ -128,8 +153,7 @@ static VALUE perform_export(
128
153
  additional_tags,
129
154
  endpoints_stats,
130
155
  &internal_metadata,
131
- &info,
132
- timeout_milliseconds
156
+ &info
133
157
  );
134
158
 
135
159
  if (build_result.tag == DDOG_PROF_EXPORTER_REQUEST_BUILD_RESULT_ERR) {
@@ -254,6 +278,15 @@ static VALUE _native_do_export(
254
278
  VALUE failure_tuple = handle_exporter_failure(exporter_result);
255
279
  if (!NIL_P(failure_tuple)) return failure_tuple;
256
280
 
281
+ ddog_prof_MaybeError timeout_result = ddog_prof_Exporter_set_timeout(exporter_result.ok, timeout_milliseconds);
282
+ if (timeout_result.tag == DDOG_PROF_OPTION_ERROR_SOME_ERROR) {
283
+ // NOTE: Seems a bit harsh to fail the upload if we can't set a timeout. OTOH, this is only expected to fail
284
+ // if the exporter is not well built. Because such a situation should already be caught above I think it's
285
+ // preferable to leave this here as a virtually unreachable exception rather than ignoring it.
286
+ ddog_prof_Exporter_drop(exporter_result.ok);
287
+ return rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&timeout_result.some));
288
+ }
289
+
257
290
  return perform_export(
258
291
  exporter_result.ok,
259
292
  start,
@@ -262,8 +295,7 @@ static VALUE _native_do_export(
262
295
  files_to_export_unmodified,
263
296
  null_additional_tags,
264
297
  internal_metadata,
265
- info,
266
- timeout_milliseconds
298
+ info
267
299
  );
268
300
  }
269
301
 
@@ -182,7 +182,7 @@ uint64_t native_thread_id_for(VALUE thread) {
182
182
  #if !defined(NO_THREAD_TID) && defined(RB_THREAD_T_HAS_NATIVE_ID)
183
183
  #ifndef NO_RB_NATIVE_THREAD
184
184
  struct rb_native_thread* native_thread = thread_struct_from_object(thread)->nt;
185
- if (native_thread == NULL) rb_raise(rb_eRuntimeError, "BUG: rb_native_thread* is null. Is this Ruby running with RUBY_MN_THREADS=1?");
185
+ if (native_thread == NULL) return 0;
186
186
  return native_thread->tid;
187
187
  #else
188
188
  return thread_struct_from_object(thread)->tid;
@@ -755,3 +755,54 @@ static inline int ddtrace_imemo_type(VALUE imemo) {
755
755
  return GET_VM()->objspace;
756
756
  }
757
757
  #endif
758
+
759
+ #ifdef USE_GVL_PROFILING_3_2_WORKAROUNDS // Ruby 3.2
760
+ #include "gvl_profiling_helper.h"
761
+
762
+ gvl_profiling_thread thread_from_thread_object(VALUE thread) {
763
+ return (gvl_profiling_thread) {.thread = thread_struct_from_object(thread)};
764
+ }
765
+
766
+ // Hack: In Ruby 3.3+ we attach gvl profiling state to Ruby threads using the
767
+ // rb_internal_thread_specific_* APIs. These APIs did not exist on Ruby 3.2. On Ruby 3.2 we instead store the
768
+ // needed data inside the `rb_thread_t` structure, specifically in `stat_insn_usage` as a Ruby FIXNUM.
769
+ //
770
+ // Why `stat_insn_usage`? We needed some per-thread storage, and while looking at the Ruby VM sources I noticed
771
+ // that `stat_insn_usage` has been in `rb_thread_t` for a long time, but is not used anywhere in the VM
772
+ // code. There's a comment attached to it "/* statistics data for profiler */" but other than marking this
773
+ // field for GC, I could not find any place in the VM commit history or on GitHub where this has ever been used.
774
+ //
775
+ // Thus, since this hack is only for 3.2, which presumably will never see this field either removed or used
776
+ // during its remaining maintenance release period we... kinda take it for our own usage. It's ugly, I know...
777
+ intptr_t gvl_profiling_state_get(gvl_profiling_thread thread) {
778
+ if (thread.thread == NULL) return 0;
779
+
780
+ VALUE current_value = ((rb_thread_t *)thread.thread)->stat_insn_usage;
781
+ intptr_t result = current_value == Qnil ? 0 : FIX2LONG(current_value);
782
+ return result;
783
+ }
784
+
785
+ void gvl_profiling_state_set(gvl_profiling_thread thread, intptr_t value) {
786
+ if (thread.thread == NULL) return;
787
+ ((rb_thread_t *)thread.thread)->stat_insn_usage = LONG2FIX(value);
788
+ }
789
+
790
+ // Because Ruby 3.2 does not give us the current thread when calling the RUBY_INTERNAL_THREAD_EVENT_READY and
791
+ // RUBY_INTERNAL_THREAD_EVENT_RESUMED APIs, we need to figure out this info ourselves.
792
+ //
793
+ // Specifically, this method was created to be called from a RUBY_INTERNAL_THREAD_EVENT_RESUMED callback --
794
+ // when it's triggered, we know the thread the code gets executed on is holding the GVL, so we use this
795
+ // opportunity to initialize our thread-local value.
796
+ gvl_profiling_thread gvl_profiling_state_maybe_initialize(void) {
797
+ gvl_profiling_thread current_thread = gvl_waiting_tls;
798
+
799
+ if (current_thread.thread == NULL) {
800
+ // threads.sched.running is the thread currently holding the GVL, which when this gets executed is the
801
+ // current thread!
802
+ current_thread = (gvl_profiling_thread) {.thread = (void *) rb_current_ractor()->threads.sched.running};
803
+ gvl_waiting_tls = current_thread;
804
+ }
805
+
806
+ return current_thread;
807
+ }
808
+ #endif
@@ -65,3 +65,6 @@ const char *imemo_kind(VALUE imemo);
65
65
  #ifdef NO_POSTPONED_TRIGGER
66
66
  void *objspace_ptr_for_gc_finalize_deferred_workaround(void);
67
67
  #endif
68
+
69
+ #define ENFORCE_THREAD(value) \
70
+ { if (RB_UNLIKELY(!rb_typeddata_is_kind_of(value, RTYPEDDATA_TYPE(rb_thread_current())))) raise_unexpected_type(value, ADD_QUOTES(value), "Thread", __FILE__, __LINE__, __func__); }
@@ -253,7 +253,7 @@ static VALUE _native_enforce_success(DDTRACE_UNUSED VALUE _self, VALUE syserr_er
253
253
 
254
254
  static void *trigger_enforce_success(void *trigger_args) {
255
255
  intptr_t syserr_errno = (intptr_t) trigger_args;
256
- ENFORCE_SUCCESS_NO_GVL(syserr_errno);
256
+ ENFORCE_SUCCESS_NO_GVL((int) syserr_errno);
257
257
  return NULL;
258
258
  }
259
259
 
@@ -19,6 +19,7 @@ typedef struct sample_labels {
19
19
  // This is used to allow the `Collectors::Stack` to modify the existing label, if any. This MUST be NULL or point
20
20
  // somewhere inside the labels slice above.
21
21
  ddog_prof_Label *state_label;
22
+ bool is_gvl_waiting_state;
22
23
 
23
24
  int64_t end_timestamp_ns;
24
25
  } sample_labels;
@@ -1,5 +1,5 @@
1
1
  #include <ruby.h>
2
- #include <datadog/profiling.h>
2
+ #include <datadog/crashtracker.h>
3
3
 
4
4
  #include "datadog_ruby_common.h"
5
5
 
@@ -28,8 +28,9 @@ void crashtracker_init(VALUE crashtracking_module) {
28
28
  static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
29
29
  VALUE options;
30
30
  rb_scan_args(argc, argv, "0:", &options);
31
+ if (options == Qnil) options = rb_hash_new();
31
32
 
32
- VALUE exporter_configuration = rb_hash_fetch(options, ID2SYM(rb_intern("exporter_configuration")));
33
+ VALUE agent_base_url = rb_hash_fetch(options, ID2SYM(rb_intern("agent_base_url")));
33
34
  VALUE path_to_crashtracking_receiver_binary = rb_hash_fetch(options, ID2SYM(rb_intern("path_to_crashtracking_receiver_binary")));
34
35
  VALUE ld_library_path = rb_hash_fetch(options, ID2SYM(rb_intern("ld_library_path")));
35
36
  VALUE tags_as_array = rb_hash_fetch(options, ID2SYM(rb_intern("tags_as_array")));
@@ -39,7 +40,7 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
39
40
  VALUE start_action = ID2SYM(rb_intern("start"));
40
41
  VALUE update_on_fork_action = ID2SYM(rb_intern("update_on_fork"));
41
42
 
42
- ENFORCE_TYPE(exporter_configuration, T_ARRAY);
43
+ ENFORCE_TYPE(agent_base_url, T_STRING);
43
44
  ENFORCE_TYPE(tags_as_array, T_ARRAY);
44
45
  ENFORCE_TYPE(path_to_crashtracking_receiver_binary, T_STRING);
45
46
  ENFORCE_TYPE(ld_library_path, T_STRING);
@@ -49,13 +50,13 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
49
50
  if (action != start_action && action != update_on_fork_action) rb_raise(rb_eArgError, "Unexpected action: %+"PRIsVALUE, action);
50
51
 
51
52
  VALUE version = datadog_gem_version();
52
- ddog_prof_Endpoint endpoint = endpoint_from(exporter_configuration);
53
53
 
54
- // Tags are heap-allocated, so after here we can't raise exceptions otherwise we'll leak this memory
54
+ // Tags and endpoint are heap-allocated, so after here we can't raise exceptions otherwise we'll leak this memory
55
55
  // Start of exception-free zone to prevent leaks {{
56
+ ddog_Endpoint *endpoint = ddog_endpoint_from_url(char_slice_from_ruby_string(agent_base_url));
56
57
  ddog_Vec_Tag tags = convert_tags(tags_as_array);
57
58
 
58
- ddog_prof_CrashtrackerConfiguration config = {
59
+ ddog_crasht_Config config = {
59
60
  .additional_files = {},
60
61
  // The Ruby VM already uses an alt stack to detect stack overflows so the crash handler must not overwrite it.
61
62
  //
@@ -67,26 +68,26 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
67
68
  // "Process.kill('SEGV', Process.pid)" gets run.
68
69
  .create_alt_stack = false,
69
70
  .endpoint = endpoint,
70
- .resolve_frames = DDOG_PROF_STACKTRACE_COLLECTION_ENABLED_WITH_SYMBOLS_IN_RECEIVER,
71
+ .resolve_frames = DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_SYMBOLS_IN_RECEIVER,
71
72
  .timeout_secs = FIX2INT(upload_timeout_seconds),
72
73
  // Waits for crash tracker to finish reporting the issue before letting the Ruby process die; see
73
74
  // https://github.com/DataDog/libdatadog/pull/477 for details
74
75
  .wait_for_receiver = true,
75
76
  };
76
77
 
77
- ddog_prof_CrashtrackerMetadata metadata = {
78
- .profiling_library_name = DDOG_CHARSLICE_C("dd-trace-rb"),
79
- .profiling_library_version = char_slice_from_ruby_string(version),
78
+ ddog_crasht_Metadata metadata = {
79
+ .library_name = DDOG_CHARSLICE_C("dd-trace-rb"),
80
+ .library_version = char_slice_from_ruby_string(version),
80
81
  .family = DDOG_CHARSLICE_C("ruby"),
81
82
  .tags = &tags,
82
83
  };
83
84
 
84
- ddog_prof_EnvVar ld_library_path_env = {
85
+ ddog_crasht_EnvVar ld_library_path_env = {
85
86
  .key = DDOG_CHARSLICE_C("LD_LIBRARY_PATH"),
86
87
  .val = char_slice_from_ruby_string(ld_library_path),
87
88
  };
88
89
 
89
- ddog_prof_CrashtrackerReceiverConfig receiver_config = {
90
+ ddog_crasht_ReceiverConfig receiver_config = {
90
91
  .args = {},
91
92
  .env = {.ptr = &ld_library_path_env, .len = 1},
92
93
  .path_to_receiver_binary = char_slice_from_ruby_string(path_to_crashtracking_receiver_binary),
@@ -94,16 +95,17 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
94
95
  .optional_stdout_filename = {},
95
96
  };
96
97
 
97
- ddog_prof_CrashtrackerResult result =
98
+ ddog_crasht_Result result =
98
99
  action == start_action ?
99
- ddog_prof_Crashtracker_init_with_receiver(config, receiver_config, metadata) :
100
- ddog_prof_Crashtracker_update_on_fork(config, receiver_config, metadata);
100
+ ddog_crasht_init_with_receiver(config, receiver_config, metadata) :
101
+ ddog_crasht_update_on_fork(config, receiver_config, metadata);
101
102
 
102
103
  // Clean up before potentially raising any exceptions
103
104
  ddog_Vec_Tag_drop(tags);
105
+ ddog_endpoint_drop(endpoint);
104
106
  // }} End of exception-free zone to prevent leaks
105
107
 
106
- if (result.tag == DDOG_PROF_CRASHTRACKER_RESULT_ERR) {
108
+ if (result.tag == DDOG_CRASHT_RESULT_ERR) {
107
109
  rb_raise(rb_eRuntimeError, "Failed to start/update the crash tracker: %"PRIsVALUE, get_error_details_and_drop(&result.err));
108
110
  }
109
111
 
@@ -111,9 +113,9 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
111
113
  }
112
114
 
113
115
  static VALUE _native_stop(DDTRACE_UNUSED VALUE _self) {
114
- ddog_prof_CrashtrackerResult result = ddog_prof_Crashtracker_shutdown();
116
+ ddog_crasht_Result result = ddog_crasht_shutdown();
115
117
 
116
- if (result.tag == DDOG_PROF_CRASHTRACKER_RESULT_ERR) {
118
+ if (result.tag == DDOG_CRASHT_RESULT_ERR) {
117
119
  rb_raise(rb_eRuntimeError, "Failed to stop the crash tracker: %"PRIsVALUE, get_error_details_and_drop(&result.err));
118
120
  }
119
121