ddtrace 1.6.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -2
  3. data/README.md +2 -2
  4. data/ext/ddtrace_profiling_loader/extconf.rb +5 -2
  5. data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +1 -1
  6. data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +3 -2
  7. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.c +81 -47
  8. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.h +1 -1
  9. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +332 -125
  10. data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.c +142 -0
  11. data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.h +14 -0
  12. data/ext/ddtrace_profiling_native_extension/collectors_idle_sampling_helper.c +241 -0
  13. data/ext/ddtrace_profiling_native_extension/collectors_idle_sampling_helper.h +3 -0
  14. data/ext/ddtrace_profiling_native_extension/collectors_stack.c +11 -13
  15. data/ext/ddtrace_profiling_native_extension/extconf.rb +22 -8
  16. data/ext/ddtrace_profiling_native_extension/helpers.h +5 -0
  17. data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +8 -0
  18. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +111 -26
  19. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +9 -0
  20. data/ext/ddtrace_profiling_native_extension/profiling.c +205 -0
  21. data/ext/ddtrace_profiling_native_extension/ruby_helpers.c +86 -0
  22. data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +28 -6
  23. data/ext/ddtrace_profiling_native_extension/setup_signal_handler.c +115 -0
  24. data/ext/ddtrace_profiling_native_extension/setup_signal_handler.h +11 -0
  25. data/ext/ddtrace_profiling_native_extension/stack_recorder.c +84 -35
  26. data/ext/ddtrace_profiling_native_extension/stack_recorder.h +1 -0
  27. data/ext/ddtrace_profiling_native_extension/time_helpers.c +17 -0
  28. data/ext/ddtrace_profiling_native_extension/time_helpers.h +10 -0
  29. data/lib/datadog/appsec/assets/blocked.html +98 -3
  30. data/lib/datadog/appsec/assets/blocked.json +1 -0
  31. data/lib/datadog/appsec/assets/blocked.text +5 -0
  32. data/lib/datadog/appsec/assets/waf_rules/recommended.json +35 -46
  33. data/lib/datadog/appsec/assets/waf_rules/risky.json +1 -1
  34. data/lib/datadog/appsec/assets/waf_rules/strict.json +46 -1
  35. data/lib/datadog/appsec/assets.rb +2 -2
  36. data/lib/datadog/appsec/configuration/settings.rb +6 -0
  37. data/lib/datadog/appsec/configuration.rb +4 -0
  38. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +4 -8
  39. data/lib/datadog/appsec/contrib/rack/request.rb +17 -0
  40. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +2 -2
  41. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +2 -2
  42. data/lib/datadog/appsec/contrib/rails/patcher.rb +3 -6
  43. data/lib/datadog/appsec/contrib/sinatra/ext.rb +1 -0
  44. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +1 -1
  45. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +11 -8
  46. data/lib/datadog/appsec/extensions.rb +10 -0
  47. data/lib/datadog/appsec/processor.rb +18 -0
  48. data/lib/datadog/appsec/response.rb +54 -0
  49. data/lib/datadog/core/configuration/components.rb +27 -6
  50. data/lib/datadog/core/configuration/ext.rb +18 -0
  51. data/lib/datadog/core/configuration/settings.rb +14 -341
  52. data/lib/datadog/core/diagnostics/health.rb +4 -22
  53. data/lib/datadog/core/environment/variable_helpers.rb +58 -10
  54. data/lib/datadog/core/runtime/ext.rb +1 -1
  55. data/lib/datadog/core/utils.rb +0 -21
  56. data/lib/datadog/core.rb +21 -1
  57. data/lib/datadog/opentracer/distributed_headers.rb +7 -9
  58. data/lib/datadog/opentracer/rack_propagator.rb +0 -3
  59. data/lib/datadog/opentracer/text_map_propagator.rb +5 -7
  60. data/lib/datadog/profiling/collectors/cpu_and_wall_time.rb +10 -4
  61. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +20 -5
  62. data/lib/datadog/profiling/collectors/dynamic_sampling_rate.rb +14 -0
  63. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +68 -0
  64. data/lib/datadog/profiling/collectors/old_stack.rb +7 -0
  65. data/lib/datadog/profiling/exporter.rb +5 -0
  66. data/lib/datadog/profiling/old_recorder.rb +8 -0
  67. data/lib/datadog/profiling/profiler.rb +7 -0
  68. data/lib/datadog/profiling/scheduler.rb +4 -7
  69. data/lib/datadog/profiling/stack_recorder.rb +36 -0
  70. data/lib/datadog/profiling/tasks/setup.rb +0 -7
  71. data/lib/datadog/profiling.rb +2 -0
  72. data/lib/datadog/tracing/configuration/ext.rb +33 -3
  73. data/lib/datadog/tracing/configuration/settings.rb +433 -0
  74. data/lib/datadog/tracing/contrib/aws/configuration/settings.rb +4 -1
  75. data/lib/datadog/tracing/contrib/aws/ext.rb +1 -0
  76. data/lib/datadog/tracing/contrib/dalli/configuration/settings.rb +4 -1
  77. data/lib/datadog/tracing/contrib/dalli/ext.rb +1 -0
  78. data/lib/datadog/tracing/contrib/delayed_job/plugin.rb +4 -0
  79. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +5 -1
  80. data/lib/datadog/tracing/contrib/elasticsearch/ext.rb +1 -0
  81. data/lib/datadog/tracing/contrib/ethon/configuration/settings.rb +6 -1
  82. data/lib/datadog/tracing/contrib/ethon/ext.rb +1 -0
  83. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +5 -1
  84. data/lib/datadog/tracing/contrib/excon/ext.rb +1 -0
  85. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +5 -1
  86. data/lib/datadog/tracing/contrib/faraday/ext.rb +1 -0
  87. data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +6 -1
  88. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +2 -1
  89. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +6 -12
  90. data/lib/datadog/tracing/contrib/grpc/distributed/fetcher.rb +27 -0
  91. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +43 -0
  92. data/lib/datadog/tracing/contrib/grpc/ext.rb +1 -0
  93. data/lib/datadog/tracing/contrib/grpc/patcher.rb +0 -2
  94. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +6 -1
  95. data/lib/datadog/tracing/contrib/http/distributed/fetcher.rb +32 -0
  96. data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +38 -0
  97. data/lib/datadog/tracing/contrib/http/ext.rb +1 -0
  98. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +6 -1
  99. data/lib/datadog/tracing/contrib/httpclient/ext.rb +1 -0
  100. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +6 -1
  101. data/lib/datadog/tracing/contrib/httprb/ext.rb +1 -0
  102. data/lib/datadog/tracing/contrib/kafka/consumer_event.rb +1 -0
  103. data/lib/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rb +1 -0
  104. data/lib/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rb +1 -0
  105. data/lib/datadog/tracing/contrib/mongodb/configuration/settings.rb +5 -1
  106. data/lib/datadog/tracing/contrib/mongodb/ext.rb +1 -0
  107. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  108. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +4 -1
  109. data/lib/datadog/tracing/contrib/mysql2/ext.rb +1 -0
  110. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -2
  111. data/lib/datadog/tracing/contrib/patcher.rb +3 -2
  112. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +4 -1
  113. data/lib/datadog/tracing/contrib/pg/ext.rb +1 -0
  114. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -2
  115. data/lib/datadog/tracing/contrib/presto/configuration/settings.rb +4 -1
  116. data/lib/datadog/tracing/contrib/presto/ext.rb +1 -0
  117. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +1 -0
  118. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +10 -12
  119. data/lib/datadog/tracing/contrib/que/tracer.rb +2 -0
  120. data/lib/datadog/tracing/contrib/racecar/events/batch.rb +4 -1
  121. data/lib/datadog/tracing/contrib/racecar/events/message.rb +4 -1
  122. data/lib/datadog/tracing/contrib/rack/middlewares.rb +2 -0
  123. data/lib/datadog/tracing/contrib/redis/configuration/settings.rb +4 -1
  124. data/lib/datadog/tracing/contrib/redis/ext.rb +1 -0
  125. data/lib/datadog/tracing/contrib/redis/instrumentation.rb +30 -21
  126. data/lib/datadog/tracing/contrib/redis/integration.rb +34 -2
  127. data/lib/datadog/tracing/contrib/redis/patcher.rb +18 -14
  128. data/lib/datadog/tracing/contrib/redis/quantize.rb +12 -9
  129. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -6
  130. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +72 -0
  131. data/lib/datadog/tracing/contrib/resque/resque_job.rb +2 -0
  132. data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +6 -1
  133. data/lib/datadog/tracing/contrib/rest_client/ext.rb +1 -0
  134. data/lib/datadog/tracing/contrib/shoryuken/tracer.rb +2 -0
  135. data/lib/datadog/tracing/contrib/sidekiq/client_tracer.rb +5 -0
  136. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +5 -0
  137. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +2 -0
  138. data/lib/datadog/{core → tracing}/diagnostics/ext.rb +1 -6
  139. data/lib/datadog/tracing/diagnostics/health.rb +40 -0
  140. data/lib/datadog/tracing/distributed/b3_multi.rb +66 -0
  141. data/lib/datadog/tracing/distributed/b3_single.rb +66 -0
  142. data/lib/datadog/tracing/distributed/datadog.rb +153 -0
  143. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +1 -0
  144. data/lib/datadog/tracing/distributed/fetcher.rb +30 -0
  145. data/lib/datadog/tracing/distributed/headers/ext.rb +18 -16
  146. data/lib/datadog/tracing/distributed/helpers.rb +9 -7
  147. data/lib/datadog/tracing/distributed/none.rb +19 -0
  148. data/lib/datadog/tracing/distributed/propagation.rb +127 -0
  149. data/lib/datadog/tracing/distributed/trace_context.rb +369 -0
  150. data/lib/datadog/tracing/metadata/ext.rb +1 -1
  151. data/lib/datadog/tracing/propagation/http.rb +3 -106
  152. data/lib/datadog/tracing/sampling/priority_sampler.rb +11 -0
  153. data/lib/datadog/tracing/sampling/rate_sampler.rb +3 -3
  154. data/lib/datadog/tracing/span.rb +3 -19
  155. data/lib/datadog/tracing/span_operation.rb +5 -4
  156. data/lib/datadog/tracing/trace_digest.rb +75 -2
  157. data/lib/datadog/tracing/trace_operation.rb +5 -4
  158. data/lib/datadog/tracing/trace_segment.rb +1 -1
  159. data/lib/datadog/tracing/utils.rb +50 -0
  160. data/lib/ddtrace/transport/trace_formatter.rb +2 -5
  161. data/lib/ddtrace/version.rb +2 -2
  162. metadata +35 -15
  163. data/lib/datadog/tracing/distributed/headers/b3.rb +0 -55
  164. data/lib/datadog/tracing/distributed/headers/b3_single.rb +0 -67
  165. data/lib/datadog/tracing/distributed/headers/datadog.rb +0 -144
  166. data/lib/datadog/tracing/distributed/headers/parser.rb +0 -37
  167. data/lib/datadog/tracing/distributed/metadata/b3.rb +0 -55
  168. data/lib/datadog/tracing/distributed/metadata/b3_single.rb +0 -66
  169. data/lib/datadog/tracing/distributed/metadata/datadog.rb +0 -73
  170. data/lib/datadog/tracing/distributed/metadata/parser.rb +0 -34
  171. data/lib/datadog/tracing/propagation/grpc.rb +0 -98
@@ -0,0 +1,142 @@
1
+ #include <ruby.h>
2
+
3
+ #include "collectors_dynamic_sampling_rate.h"
4
+ #include "helpers.h"
5
+ #include "ruby_helpers.h"
6
+ #include "time_helpers.h"
7
+
8
+ // Used to pace the rate of profiling samples based on the last observed time for a sample.
9
+ //
10
+ // This file implements the native bits of the Datadog::Profiling::Collectors::DynamicSamplingRate module, and is
11
+ // only exposed to Ruby for testing (it's always and only invoked by other C code in production).
12
+
13
+ // ---
14
+ // ## Dynamic Sampling Rate
15
+ //
16
+ // Our profilers get deployed in quite unpredictable situations in terms of system resources. While they can provide key
17
+ // information to help customers solve their performance problems, the profilers must always be careful not to make
18
+ // performance problems worse. This is where the idea of a dynamic sampling rate comes in.
19
+ //
20
+ // Instead of sampling at a fixed sample rate, the actual sampling rate should be decided by also observing the impact
21
+ // that running the profiler is having. This protects against issues such as the profiler being deployed in very busy
22
+ //machines or containers with unrealistic CPU restrictions.
23
+ //
24
+ // ### Implementation
25
+ //
26
+ // The APIs exposed by this file are used by the `CpuAndWallTimeWorker`.
27
+ //
28
+ // The main idea of the implementation below is the following: whenever the profiler takes a sample, the time we spent
29
+ // sampling and the current wall-time are recorded by calling `dynamic_sampling_rate_after_sample()`.
30
+ //
31
+ // Inside `dynamic_sampling_rate_after_sample()`, both values are combined to decide a future wall-time before which
32
+ // we should not sample. That is, we may decide that the next sample should happen no less than 200ms from now.
33
+ //
34
+ // Before taking a sample, the profiler checks using `dynamic_sampling_rate_should_sample()`, if it's time or not to
35
+ // sample. If it's not, it will skip sampling.
36
+ //
37
+ // Finally, as an additional optimization, there's a `dynamic_sampling_rate_get_sleep()` which, given the current
38
+ // wall-time, will return the time remaining (*there's an exception, check below) until the next sample.
39
+ //
40
+ // ---
41
+
42
+ // This is the wall-time overhead we're targeting. E.g. we target to spend no more than 2%, or 1.2 seconds per minute,
43
+ // taking profiling samples.
44
+ #define WALL_TIME_OVERHEAD_TARGET_PERCENTAGE 2.0 // %
45
+ // See `dynamic_sampling_rate_get_sleep()` for details
46
+ #define MAX_SLEEP_TIME_NS MILLIS_AS_NS(100)
47
+ // See `dynamic_sampling_rate_after_sample()` for details
48
+ #define MAX_TIME_UNTIL_NEXT_SAMPLE_NS SECONDS_AS_NS(10)
49
+
50
+ void dynamic_sampling_rate_init(dynamic_sampling_rate_state *state) {
51
+ atomic_init(&state->next_sample_after_monotonic_wall_time_ns, 0);
52
+ }
53
+
54
+ void dynamic_sampling_rate_reset(dynamic_sampling_rate_state *state) {
55
+ atomic_store(&state->next_sample_after_monotonic_wall_time_ns, 0);
56
+ }
57
+
58
+ uint64_t dynamic_sampling_rate_get_sleep(dynamic_sampling_rate_state *state, long current_monotonic_wall_time_ns) {
59
+ long next_sample_after_ns = atomic_load(&state->next_sample_after_monotonic_wall_time_ns);
60
+ long delta_ns = next_sample_after_ns - current_monotonic_wall_time_ns;
61
+
62
+ if (delta_ns > 0 && next_sample_after_ns > 0) {
63
+ // We don't want to sleep for too long as the profiler may be trying to stop.
64
+ //
65
+ // Instead, here we sleep for at most this time. Worst case, the profiler will still try to sample before
66
+ // `next_sample_after_monotonic_wall_time_ns`, BUT `dynamic_sampling_rate_should_sample()` will still be false
67
+ // so we still get the intended behavior.
68
+ return uint64_min_of(delta_ns, MAX_SLEEP_TIME_NS);
69
+ } else {
70
+ return 0;
71
+ }
72
+ }
73
+
74
+ bool dynamic_sampling_rate_should_sample(dynamic_sampling_rate_state *state, long wall_time_ns_before_sample) {
75
+ return wall_time_ns_before_sample >= atomic_load(&state->next_sample_after_monotonic_wall_time_ns);
76
+ }
77
+
78
+ void dynamic_sampling_rate_after_sample(dynamic_sampling_rate_state *state, long wall_time_ns_after_sample, uint64_t sampling_time_ns) {
79
+ double overhead_target = (double) WALL_TIME_OVERHEAD_TARGET_PERCENTAGE;
80
+
81
+ // The idea here is that we're targeting a maximum % of wall-time spent sampling.
82
+ // So for instance, if sampling_time_ns is 2% of the time we spend working, how much is the 98% we should spend
83
+ // sleeping? As an example, if the last sample took 1ms and the target overhead is 2%, we should sleep for 49ms.
84
+ uint64_t time_to_sleep_ns = sampling_time_ns * ((100.0 - overhead_target)/overhead_target);
85
+
86
+ // In case a sample took an unexpected long time (e.g. maybe a VM was paused, or a laptop was suspended), we clamp the
87
+ // value so it doesn't get too crazy
88
+ time_to_sleep_ns = uint64_min_of(time_to_sleep_ns, MAX_TIME_UNTIL_NEXT_SAMPLE_NS);
89
+
90
+ atomic_store(&state->next_sample_after_monotonic_wall_time_ns, wall_time_ns_after_sample + time_to_sleep_ns);
91
+ }
92
+
93
+ // ---
94
+ // Below here is boilerplate to expose the above code to Ruby so that we can test it with RSpec as usual.
95
+
96
+ VALUE _native_get_sleep(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE current_monotonic_wall_time_ns);
97
+ VALUE _native_should_sample(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE wall_time_ns_before_sample);
98
+ VALUE _native_after_sample(DDTRACE_UNUSED VALUE self, VALUE wall_time_ns_after_sample, VALUE sampling_time_ns);
99
+
100
+ void collectors_dynamic_sampling_rate_init(VALUE profiling_module) {
101
+ VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
102
+ VALUE dynamic_sampling_rate_module = rb_define_module_under(collectors_module, "DynamicSamplingRate");
103
+ VALUE testing_module = rb_define_module_under(dynamic_sampling_rate_module, "Testing");
104
+
105
+ rb_define_singleton_method(testing_module, "_native_get_sleep", _native_get_sleep, 2);
106
+ rb_define_singleton_method(testing_module, "_native_should_sample", _native_should_sample, 2);
107
+ rb_define_singleton_method(testing_module, "_native_after_sample", _native_after_sample, 2);
108
+ }
109
+
110
+ VALUE _native_get_sleep(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE current_monotonic_wall_time_ns) {
111
+ ENFORCE_TYPE(simulated_next_sample_after_monotonic_wall_time_ns, T_FIXNUM);
112
+ ENFORCE_TYPE(current_monotonic_wall_time_ns, T_FIXNUM);
113
+
114
+ dynamic_sampling_rate_state state;
115
+ dynamic_sampling_rate_init(&state);
116
+ atomic_store(&state.next_sample_after_monotonic_wall_time_ns, NUM2LONG(simulated_next_sample_after_monotonic_wall_time_ns));
117
+
118
+ return ULL2NUM(dynamic_sampling_rate_get_sleep(&state, NUM2LONG(current_monotonic_wall_time_ns)));
119
+ }
120
+
121
+ VALUE _native_should_sample(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE wall_time_ns_before_sample) {
122
+ ENFORCE_TYPE(simulated_next_sample_after_monotonic_wall_time_ns, T_FIXNUM);
123
+ ENFORCE_TYPE(wall_time_ns_before_sample, T_FIXNUM);
124
+
125
+ dynamic_sampling_rate_state state;
126
+ dynamic_sampling_rate_init(&state);
127
+ atomic_store(&state.next_sample_after_monotonic_wall_time_ns, NUM2LONG(simulated_next_sample_after_monotonic_wall_time_ns));
128
+
129
+ return dynamic_sampling_rate_should_sample(&state, NUM2LONG(wall_time_ns_before_sample)) ? Qtrue : Qfalse;
130
+ }
131
+
132
+ VALUE _native_after_sample(DDTRACE_UNUSED VALUE self, VALUE wall_time_ns_after_sample, VALUE sampling_time_ns) {
133
+ ENFORCE_TYPE(wall_time_ns_after_sample, T_FIXNUM);
134
+ ENFORCE_TYPE(sampling_time_ns, T_FIXNUM);
135
+
136
+ dynamic_sampling_rate_state state;
137
+ dynamic_sampling_rate_init(&state);
138
+
139
+ dynamic_sampling_rate_after_sample(&state, NUM2LONG(wall_time_ns_after_sample), NUM2ULL(sampling_time_ns));
140
+
141
+ return ULL2NUM(atomic_load(&state.next_sample_after_monotonic_wall_time_ns));
142
+ }
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ #include <stdatomic.h>
4
+ #include <stdbool.h>
5
+
6
+ typedef struct {
7
+ atomic_long next_sample_after_monotonic_wall_time_ns;
8
+ } dynamic_sampling_rate_state;
9
+
10
+ void dynamic_sampling_rate_init(dynamic_sampling_rate_state *state);
11
+ void dynamic_sampling_rate_reset(dynamic_sampling_rate_state *state);
12
+ uint64_t dynamic_sampling_rate_get_sleep(dynamic_sampling_rate_state *state, long current_monotonic_wall_time_ns);
13
+ bool dynamic_sampling_rate_should_sample(dynamic_sampling_rate_state *state, long wall_time_ns_before_sample);
14
+ void dynamic_sampling_rate_after_sample(dynamic_sampling_rate_state *state, long wall_time_ns_after_sample, uint64_t sampling_time_ns);
@@ -0,0 +1,241 @@
1
+ #include <ruby.h>
2
+ #include <ruby/thread.h>
3
+ #include <pthread.h>
4
+ #include <stdbool.h>
5
+
6
+ #include "helpers.h"
7
+ #include "ruby_helpers.h"
8
+ #include "collectors_idle_sampling_helper.h"
9
+
10
+ // Used by the Collectors::CpuAndWallTimeWorker to gather samples when the Ruby process is idle.
11
+ //
12
+ // Specifically, the IdleSamplingHelper is expected to be triggered by the CpuAndWallTimeWorker whenever it needs to
13
+ // trigger a sample, but the VM is otherwise idle. See implementation of CpuAndWallTimeWorker for details.
14
+ //
15
+ // The IdleSamplingHelper keeps a background thread that waits for functions to run on a single-element "queue".
16
+ // Other threads communicate with it by asking it to ACTION_RUN a `requested_action` or ACTION_STOP to terminate.
17
+ //
18
+ // The state is protected by the `wakeup_mutex`, and the background thread is woken up after changes using the
19
+ // `wakeup` condition variable.
20
+
21
+ typedef enum { ACTION_WAIT, ACTION_RUN, ACTION_STOP } action;
22
+
23
+ // Contains state for a single CpuAndWallTimeWorker instance
24
+ struct idle_sampling_loop_state {
25
+ pthread_mutex_t wakeup_mutex;
26
+ pthread_cond_t wakeup;
27
+ action requested_action;
28
+ void (*run_action_function)(void);
29
+ };
30
+
31
+ static VALUE _native_new(VALUE klass);
32
+ static void reset_state(struct idle_sampling_loop_state *state);
33
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
34
+ static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
35
+ static void *run_idle_sampling_loop(void *state_ptr);
36
+ static void interrupt_idle_sampling_loop(void *state_ptr);
37
+ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance);
38
+ static VALUE _native_idle_sampling_helper_request_action(DDTRACE_UNUSED VALUE self, VALUE self_instance);
39
+ static void *request_testing_action(void *self_instance_ptr);
40
+ static void grab_gvl_and_run_testing_action(void);
41
+ static void *run_testing_action(DDTRACE_UNUSED void *unused);
42
+
43
+ void collectors_idle_sampling_helper_init(VALUE profiling_module) {
44
+ VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
45
+ VALUE collectors_idle_sampling_helper_class = rb_define_class_under(collectors_module, "IdleSamplingHelper", rb_cObject);
46
+ // Hosts methods used for testing the native code using RSpec
47
+ VALUE testing_module = rb_define_module_under(collectors_idle_sampling_helper_class, "Testing");
48
+
49
+ // Instances of the IdleSamplingHelper class are "TypedData" objects.
50
+ // "TypedData" objects are special objects in the Ruby VM that can wrap C structs.
51
+ // In this case, it wraps the idle_sampling_loop_state.
52
+ //
53
+ // Because Ruby doesn't know how to initialize native-level structs, we MUST override the allocation function for objects
54
+ // of this class so that we can manage this part. Not overriding or disabling the allocation function is a common
55
+ // gotcha for "TypedData" objects that can very easily lead to VM crashes, see for instance
56
+ // https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
57
+ rb_define_alloc_func(collectors_idle_sampling_helper_class, _native_new);
58
+
59
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 1);
60
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_stop", _native_stop, 1);
61
+ rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_reset", _native_reset, 1);
62
+ rb_define_singleton_method(testing_module, "_native_idle_sampling_helper_request_action", _native_idle_sampling_helper_request_action, 1);
63
+ }
64
+
65
+ // This structure is used to define a Ruby object that stores a pointer to a struct idle_sampling_loop_state
66
+ // See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works
67
+ static const rb_data_type_t idle_sampling_helper_typed_data = {
68
+ .wrap_struct_name = "Datadog::Profiling::Collectors::IdleSamplingHelper",
69
+ .function = {
70
+ .dmark = NULL, // We don't store references to Ruby objects so we don't need to mark any of them
71
+ .dfree = RUBY_DEFAULT_FREE,
72
+ .dsize = NULL, // We don't track memory usage (although it'd be cool if we did!)
73
+ //.dcompact = NULL, // Not needed -- we don't store references to Ruby objects
74
+ },
75
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY
76
+ };
77
+
78
+ static VALUE _native_new(VALUE klass) {
79
+ struct idle_sampling_loop_state *state = ruby_xcalloc(1, sizeof(struct idle_sampling_loop_state));
80
+
81
+ reset_state(state);
82
+
83
+ return TypedData_Wrap_Struct(klass, &idle_sampling_helper_typed_data, state);
84
+ }
85
+
86
+ static void reset_state(struct idle_sampling_loop_state *state) {
87
+ state->wakeup_mutex = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
88
+ state->wakeup = (pthread_cond_t) PTHREAD_COND_INITIALIZER;
89
+ state->requested_action = ACTION_WAIT;
90
+ state->run_action_function = NULL;
91
+ }
92
+
93
+ // The same instance of the IdleSamplingHelper can be reused multiple times, and this resets it back to
94
+ // a pristine state before recreating the worker thread (this includes resetting the mutex in case it was left
95
+ // locked halfway through the VM forking)
96
+ static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
97
+ struct idle_sampling_loop_state *state;
98
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
99
+
100
+ reset_state(state);
101
+
102
+ return Qtrue;
103
+ }
104
+
105
+ static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
106
+ struct idle_sampling_loop_state *state;
107
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
108
+
109
+ // Release GVL and run the loop waiting for requests
110
+ rb_thread_call_without_gvl(run_idle_sampling_loop, state, interrupt_idle_sampling_loop, state);
111
+
112
+ return Qtrue;
113
+ }
114
+
115
+ static void *run_idle_sampling_loop(void *state_ptr) {
116
+ struct idle_sampling_loop_state *state = (struct idle_sampling_loop_state *) state_ptr;
117
+ int error = 0;
118
+
119
+ while (true) {
120
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
121
+
122
+ action next_action;
123
+ void (*run_action_function)(void);
124
+
125
+ // Await for an action
126
+ while ((next_action = state->requested_action) == ACTION_WAIT) {
127
+ error = pthread_cond_wait(&state->wakeup, &state->wakeup_mutex);
128
+ if (error) {
129
+ // If something went wrong, try to leave the mutex unlocked at least
130
+ pthread_mutex_unlock(&state->wakeup_mutex);
131
+ ENFORCE_SUCCESS_NO_GVL(error);
132
+ }
133
+ }
134
+
135
+ // There's an action to be taken!
136
+
137
+ // Record function, if any
138
+ run_action_function = state->run_action_function;
139
+
140
+ // Reset buffer for next request
141
+ state->requested_action = ACTION_WAIT;
142
+
143
+ // Unlock the mutex immediately so other threads can continue to request actions without blocking
144
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
145
+
146
+ // Process pending action
147
+ if (next_action == ACTION_RUN) {
148
+ if (run_action_function == NULL) {
149
+ grab_gvl_and_raise(rb_eRuntimeError, "Unexpected NULL run_action_function in run_idle_sampling_loop");
150
+ }
151
+
152
+ run_action_function();
153
+ } else { // ACTION_STOP
154
+ return NULL;
155
+ }
156
+ }
157
+ }
158
+
159
+ static void interrupt_idle_sampling_loop(void *state_ptr) {
160
+ struct idle_sampling_loop_state *state = (struct idle_sampling_loop_state *) state_ptr;
161
+ int error = 0;
162
+
163
+ // Note about the error handling in this situation: Something bad happening at this stage is really really awkward to
164
+ // handle because we get called by the VM in a situation where we can't really raise exceptions, and the VM really really
165
+ // just wants us to stop what we're doing and return control of the thread to it.
166
+ //
167
+ // So if we return immediately on error, we may leave the VM hanging because we didn't actually interrupt the thread.
168
+ // We're also not at a great location to flag errors.
169
+ // That's why: a) I chose to log to stderr, as a last-ditch effort; b) even if something goes wrong we still try to
170
+ // ask the thread to stop, instead of exiting early.
171
+
172
+ error = pthread_mutex_lock(&state->wakeup_mutex);
173
+ if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_mutex_lock in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
174
+
175
+ state->requested_action = ACTION_STOP;
176
+
177
+ error = pthread_mutex_unlock(&state->wakeup_mutex);
178
+ if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_mutex_unlock in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
179
+
180
+ error = pthread_cond_broadcast(&state->wakeup);
181
+ if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_cond_broadcast in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
182
+ }
183
+
184
+ static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
185
+ struct idle_sampling_loop_state *state;
186
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
187
+
188
+ ENFORCE_SUCCESS_GVL(pthread_mutex_lock(&state->wakeup_mutex));
189
+ state->requested_action = ACTION_STOP;
190
+ ENFORCE_SUCCESS_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
191
+
192
+ // Wake up worker thread, if needed; It's OK to call broadcast after releasing the mutex
193
+ ENFORCE_SUCCESS_GVL(pthread_cond_broadcast(&state->wakeup));
194
+
195
+ return Qtrue;
196
+ }
197
+
198
+ // Assumption: Function gets called without the global VM lock
199
+ void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_function)(void)) {
200
+ struct idle_sampling_loop_state *state;
201
+ if (!rb_typeddata_is_kind_of(self_instance, &idle_sampling_helper_typed_data)) {
202
+ grab_gvl_and_raise(rb_eTypeError, "Wrong argument for idle_sampling_helper_request_action");
203
+ }
204
+ // This should never fail the the above check passes
205
+ TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
206
+
207
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
208
+ if (state->requested_action == ACTION_WAIT) {
209
+ state->requested_action = ACTION_RUN;
210
+ state->run_action_function = run_action_function;
211
+ }
212
+ ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
213
+
214
+ // Wake up worker thread, if needed; It's OK to call broadcast after releasing the mutex
215
+ ENFORCE_SUCCESS_NO_GVL(pthread_cond_broadcast(&state->wakeup));
216
+ }
217
+
218
+ // Because the idle_sampling_helper_request_action is built to be called without the global VM lock, here we release it
219
+ // to be able to call that API.
220
+ static VALUE _native_idle_sampling_helper_request_action(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
221
+ rb_thread_call_without_gvl(request_testing_action, (void *) self_instance, NULL, NULL);
222
+ return Qtrue;
223
+ }
224
+
225
+ static void *request_testing_action(void *self_instance_ptr) {
226
+ VALUE self_instance = (VALUE) self_instance_ptr;
227
+ idle_sampling_helper_request_action(self_instance, grab_gvl_and_run_testing_action);
228
+ return NULL;
229
+ }
230
+
231
+ // This gets called by the worker thread, which is not holding the global VM lock. To be able to actually run the action,
232
+ // we need to acquire it
233
+ static void grab_gvl_and_run_testing_action(void) {
234
+ rb_thread_call_with_gvl(run_testing_action, NULL);
235
+ }
236
+
237
+ static void *run_testing_action(DDTRACE_UNUSED void *unused) {
238
+ VALUE idle_sampling_helper_testing_action = rb_gv_get("$idle_sampling_helper_testing_action");
239
+ rb_funcall(idle_sampling_helper_testing_action, rb_intern("call"), 0);
240
+ return NULL;
241
+ }
@@ -0,0 +1,3 @@
1
+ #pragma once
2
+
3
+ void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_function)(void));
@@ -144,11 +144,10 @@ void sample_thread(
144
144
 
145
145
  // Samples thread into recorder, including as a top frame in the stack a frame named "Garbage Collection"
146
146
  if (type == SAMPLE_IN_GC) {
147
+ ddog_CharSlice function_name = DDOG_CHARSLICE_C("");
148
+ ddog_CharSlice function_filename = DDOG_CHARSLICE_C("Garbage Collection");
147
149
  buffer->lines[0] = (ddog_Line) {
148
- .function = (ddog_Function) {
149
- .name = DDOG_CHARSLICE_C(""),
150
- .filename = DDOG_CHARSLICE_C("Garbage Collection")
151
- },
150
+ .function = (ddog_Function) {.name = function_name, .filename = function_filename},
152
151
  .line = 0
153
152
  };
154
153
  // To avoid changing sample_thread_internal, we just prepare a new buffer struct that uses the same underlying storage as the
@@ -300,11 +299,10 @@ static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer*
300
299
 
301
300
  // Important note: `frames_omitted_message` MUST have a lifetime that is at least as long as the call to
302
301
  // `record_sample`. So be careful where it gets allocated. (We do have tests for this, at least!)
302
+ ddog_CharSlice function_name = DDOG_CHARSLICE_C("");
303
+ ddog_CharSlice function_filename = {.ptr = frames_omitted_message, .len = strlen(frames_omitted_message)};
303
304
  buffer->lines[buffer->max_frames - 1] = (ddog_Line) {
304
- .function = (ddog_Function) {
305
- .name = DDOG_CHARSLICE_C(""),
306
- .filename = ((ddog_CharSlice) {.ptr = frames_omitted_message, .len = strlen(frames_omitted_message)})
307
- },
305
+ .function = (ddog_Function) {.name = function_name, .filename = function_filename},
308
306
  .line = 0,
309
307
  };
310
308
  }
@@ -337,11 +335,10 @@ static void record_placeholder_stack_in_native_code(
337
335
  sampling_buffer *record_buffer,
338
336
  int extra_frames_in_record_buffer
339
337
  ) {
338
+ ddog_CharSlice function_name = DDOG_CHARSLICE_C("");
339
+ ddog_CharSlice function_filename = DDOG_CHARSLICE_C("In native code");
340
340
  buffer->lines[0] = (ddog_Line) {
341
- .function = (ddog_Function) {
342
- .name = DDOG_CHARSLICE_C(""),
343
- .filename = DDOG_CHARSLICE_C("In native code")
344
- },
341
+ .function = (ddog_Function) {.name = function_name, .filename = function_filename},
345
342
  .line = 0
346
343
  };
347
344
 
@@ -373,7 +370,8 @@ sampling_buffer *sampling_buffer_new(unsigned int max_frames) {
373
370
  // Currently we have a 1-to-1 correspondence between lines and locations, so we just initialize the locations once
374
371
  // here and then only mutate the contents of the lines.
375
372
  for (unsigned int i = 0; i < max_frames; i++) {
376
- buffer->locations[i] = (ddog_Location) {.lines = (ddog_Slice_line) {.ptr = &buffer->lines[i], .len = 1}};
373
+ ddog_Slice_line lines = (ddog_Slice_line) {.ptr = &buffer->lines[i], .len = 1};
374
+ buffer->locations[i] = (ddog_Location) {.lines = lines};
377
375
  }
378
376
 
379
377
  return buffer;
@@ -67,6 +67,10 @@ $stderr.puts(
67
67
  # that may fail on an environment not properly setup for building Ruby extensions.
68
68
  require 'mkmf'
69
69
 
70
+ Logging.message(" [ddtrace] Using compiler:\n")
71
+ xsystem("#{CONFIG['CC']} -v")
72
+ Logging.message(" [ddtrace] End of compiler information\n")
73
+
70
74
  # mkmf on modern Rubies actually has an append_cflags that does something similar
71
75
  # (see https://github.com/ruby/ruby/pull/5760), but as usual we need a bit more boilerplate to deal with legacy Rubies
72
76
  def add_compiler_flag(flag)
@@ -79,7 +83,7 @@ end
79
83
 
80
84
  # Because we can't control what compiler versions our customers use, shipping with -Werror by default is a no-go.
81
85
  # But we can enable it in CI, so that we quickly spot any new warnings that just got introduced.
82
- add_compiler_flag '-Werror' if ENV['CI'] == 'true'
86
+ add_compiler_flag '-Werror' if ENV['DDTRACE_CI'] == 'true'
83
87
 
84
88
  # Older gcc releases may not default to C99 and we need to ask for this. This is also used:
85
89
  # * by upstream Ruby -- search for gnu99 in the codebase
@@ -87,7 +91,7 @@ add_compiler_flag '-Werror' if ENV['CI'] == 'true'
87
91
  # (https://github.com/msgpack/msgpack-ruby/blob/18ce08f6d612fe973843c366ac9a0b74c4e50599/ext/msgpack/extconf.rb#L8)
88
92
  add_compiler_flag '-std=gnu99'
89
93
 
90
- # Gets really noisy when we include the MJIT header, let's omit it
94
+ # Gets really noisy when we include the MJIT header, let's omit it (TODO: Use #pragma GCC diagnostic instead?)
91
95
  add_compiler_flag '-Wno-unused-function'
92
96
 
93
97
  # Allow defining variables at any point in a function
@@ -108,6 +112,9 @@ add_compiler_flag '-Wunused-parameter'
108
112
  # For more details see https://gcc.gnu.org/wiki/Visibility
109
113
  add_compiler_flag '-fvisibility=hidden'
110
114
 
115
+ # Avoid legacy C definitions
116
+ add_compiler_flag '-Wold-style-definition'
117
+
111
118
  # Enable all other compiler warnings
112
119
  add_compiler_flag '-Wall'
113
120
  add_compiler_flag '-Wextra'
@@ -126,6 +133,9 @@ end
126
133
  # On older Rubies, there was no struct rb_native_thread. See private_vm_api_acccess.c for details.
127
134
  $defs << '-DNO_RB_NATIVE_THREAD' if RUBY_VERSION < '3.2'
128
135
 
136
+ # On older Rubies, there was no struct rb_thread_sched (it was struct rb_global_vm_lock_struct)
137
+ $defs << '-DNO_RB_THREAD_SCHED' if RUBY_VERSION < '3.2'
138
+
129
139
  # On older Rubies, there was no tid member in the internal thread structure
130
140
  $defs << '-DNO_THREAD_TID' if RUBY_VERSION < '3.1'
131
141
 
@@ -135,9 +145,15 @@ $defs << '-DUSE_BACKPORTED_RB_PROFILE_FRAME_METHOD_NAME' if RUBY_VERSION < '3'
135
145
  # On older Rubies, there are no Ractors
136
146
  $defs << '-DNO_RACTORS' if RUBY_VERSION < '3'
137
147
 
148
+ # On older Rubies, rb_global_vm_lock_struct did not include the owner field
149
+ $defs << '-DNO_GVL_OWNER' if RUBY_VERSION < '2.6'
150
+
138
151
  # On older Rubies, we need to use rb_thread_t instead of rb_execution_context_t
139
152
  $defs << '-DUSE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT' if RUBY_VERSION < '2.5'
140
153
 
154
+ # On older Rubies, extensions can't use GET_VM()
155
+ $defs << '-DNO_GET_VM' if RUBY_VERSION < '2.5'
156
+
141
157
  # On older Rubies...
142
158
  if RUBY_VERSION < '2.4'
143
159
  # ...we need to use RUBY_VM_NORMAL_ISEQ_P instead of VM_FRAME_RUBYFRAME_P
@@ -154,8 +170,6 @@ if RUBY_VERSION < '2.3'
154
170
  $defs << '-DUSE_LEGACY_RB_PROFILE_FRAMES'
155
171
  # ... you couldn't name threads
156
172
  $defs << '-DNO_THREAD_NAMES'
157
- # ...the ruby_thread_has_gvl_p function was not exposed to users outside of the VM
158
- $defs << '-DNO_THREAD_HAS_GVL'
159
173
  end
160
174
 
161
175
  # If we got here, libdatadog is available and loaded
@@ -173,6 +187,10 @@ unless pkg_config('datadog_profiling_with_rpath')
173
187
  )
174
188
  end
175
189
 
190
+ unless have_type('atomic_int', ['stdatomic.h'])
191
+ skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported::COMPILER_ATOMIC_MISSING)
192
+ end
193
+
176
194
  # See comments on the helper method being used for why we need to additionally set this.
177
195
  # The extremely excessive escaping around ORIGIN below seems to be correct and was determined after a lot of
178
196
  # experimentation. We need to get these special characters across a lot of tools untouched...
@@ -181,10 +199,6 @@ $LDFLAGS += \
181
199
  "#{Datadog::Profiling::NativeExtensionHelpers.libdatadog_folder_relative_to_native_lib_folder}"
182
200
  Logging.message(" [ddtrace] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
183
201
 
184
- Logging.message(" [ddtrace] Using compiler:\n")
185
- xsystem("#{CONFIG['CC']} --version")
186
- Logging.message(" [ddtrace] End of compiler information\n")
187
-
188
202
  # Tag the native extension library with the Ruby version and Ruby platform.
189
203
  # This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
190
204
  # the wrong library is never loaded.
@@ -10,3 +10,8 @@
10
10
  #else
11
11
  #define DDTRACE_UNUSED
12
12
  #endif
13
+
14
+ // @ivoanjo: After trying to read through https://stackoverflow.com/questions/3437404/min-and-max-in-c I decided I
15
+ // don't like C and I just implemented this as a function.
16
+ inline static uint64_t uint64_max_of(uint64_t a, uint64_t b) { return a > b ? a : b; }
17
+ inline static uint64_t uint64_min_of(uint64_t a, uint64_t b) { return a > b ? b : a; }
@@ -179,6 +179,14 @@ module Datadog
179
179
  suggested: CONTACT_SUPPORT,
180
180
  )
181
181
 
182
+ # Validation for this check is done in extconf.rb because it relies on mkmf
183
+ COMPILER_ATOMIC_MISSING = explain_issue(
184
+ 'your C compiler is missing support for the <stdatomic.h> header.',
185
+ 'This issue can usually be fixed by upgrading to a later version of your',
186
+ 'operating system image or compiler.',
187
+ suggested: CONTACT_SUPPORT,
188
+ )
189
+
182
190
  private_class_method def self.disabled_via_env?
183
191
  disabled_via_env = explain_issue(
184
192
  'the `DD_PROFILING_NO_EXTENSION` environment variable is/was set to',