datadog 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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