ddtrace 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +7 -2
  5. data/ddtrace.gemspec +5 -2
  6. data/docs/GettingStarted.md +27 -3
  7. data/docs/ProfilingDevelopment.md +27 -28
  8. data/docs/UpgradeGuide.md +1 -1
  9. data/ext/ddtrace_profiling_loader/ddtrace_profiling_loader.c +1 -1
  10. data/ext/ddtrace_profiling_loader/extconf.rb +1 -0
  11. data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +6 -5
  12. data/ext/ddtrace_profiling_native_extension/clock_id.h +1 -1
  13. data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +1 -1
  14. data/ext/ddtrace_profiling_native_extension/clock_id_noop.c +1 -1
  15. data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.c +269 -0
  16. data/ext/ddtrace_profiling_native_extension/collectors_stack.c +12 -12
  17. data/ext/ddtrace_profiling_native_extension/collectors_stack.h +9 -0
  18. data/ext/ddtrace_profiling_native_extension/extconf.rb +44 -3
  19. data/ext/ddtrace_profiling_native_extension/http_transport.c +341 -0
  20. data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +92 -4
  21. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +76 -1
  22. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +3 -0
  23. data/ext/ddtrace_profiling_native_extension/profiling.c +4 -0
  24. data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +33 -0
  25. data/ext/ddtrace_profiling_native_extension/stack_recorder.c +18 -10
  26. data/ext/ddtrace_profiling_native_extension/stack_recorder.h +10 -1
  27. data/lib/datadog/core/configuration/components.rb +39 -24
  28. data/lib/datadog/core/configuration/settings.rb +8 -1
  29. data/lib/datadog/core/environment/platform.rb +40 -0
  30. data/lib/datadog/core/utils.rb +1 -1
  31. data/lib/datadog/opentracer/thread_local_scope_manager.rb +26 -3
  32. data/lib/datadog/profiling/collectors/code_provenance.rb +1 -0
  33. data/lib/datadog/profiling/collectors/cpu_and_wall_time.rb +42 -0
  34. data/lib/datadog/profiling/collectors/stack.rb +2 -0
  35. data/lib/datadog/profiling/encoding/profile.rb +7 -11
  36. data/lib/datadog/profiling/exporter.rb +58 -9
  37. data/lib/datadog/profiling/ext/forking.rb +8 -8
  38. data/lib/datadog/profiling/ext.rb +2 -15
  39. data/lib/datadog/profiling/flush.rb +25 -53
  40. data/lib/datadog/profiling/http_transport.rb +131 -0
  41. data/lib/datadog/profiling/old_ext.rb +42 -0
  42. data/lib/datadog/profiling/{recorder.rb → old_recorder.rb} +20 -31
  43. data/lib/datadog/profiling/scheduler.rb +24 -43
  44. data/lib/datadog/profiling/transport/http/api/endpoint.rb +9 -31
  45. data/lib/datadog/profiling/transport/http/client.rb +5 -3
  46. data/lib/datadog/profiling/transport/http/response.rb +0 -2
  47. data/lib/datadog/profiling/transport/http.rb +1 -1
  48. data/lib/datadog/profiling.rb +3 -3
  49. data/lib/datadog/tracing/context_provider.rb +17 -1
  50. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -0
  51. data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +1 -0
  52. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -1
  53. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor.rb +4 -0
  54. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +35 -0
  55. data/lib/datadog/tracing/contrib/pg/ext.rb +31 -0
  56. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +129 -0
  57. data/lib/datadog/tracing/contrib/pg/integration.rb +43 -0
  58. data/lib/datadog/tracing/contrib/pg/patcher.rb +31 -0
  59. data/lib/datadog/tracing/contrib/rails/configuration/settings.rb +3 -0
  60. data/lib/datadog/tracing/contrib/rails/framework.rb +2 -1
  61. data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +1 -0
  62. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -1
  63. data/lib/datadog/tracing/contrib.rb +1 -0
  64. data/lib/datadog/tracing/distributed/headers/b3.rb +1 -1
  65. data/lib/datadog/tracing/distributed/headers/b3_single.rb +4 -4
  66. data/lib/datadog/tracing/distributed/headers/datadog.rb +1 -1
  67. data/lib/datadog/tracing/distributed/headers/parser.rb +37 -0
  68. data/lib/datadog/tracing/distributed/helpers.rb +34 -0
  69. data/lib/datadog/tracing/distributed/metadata/b3.rb +55 -0
  70. data/lib/datadog/tracing/distributed/metadata/b3_single.rb +66 -0
  71. data/lib/datadog/tracing/distributed/metadata/datadog.rb +73 -0
  72. data/lib/datadog/tracing/distributed/metadata/parser.rb +34 -0
  73. data/lib/datadog/tracing/metadata/ext.rb +25 -0
  74. data/lib/datadog/tracing/metadata/tagging.rb +6 -0
  75. data/lib/datadog/tracing/propagation/grpc.rb +65 -55
  76. data/lib/datadog/tracing/sampling/rate_sampler.rb +2 -2
  77. data/lib/datadog/tracing/sampling/span/matcher.rb +80 -0
  78. data/lib/datadog/tracing/span.rb +21 -1
  79. data/lib/datadog/tracing/span_operation.rb +2 -1
  80. data/lib/ddtrace/version.rb +1 -1
  81. metadata +24 -13
  82. data/lib/datadog/profiling/transport/client.rb +0 -16
  83. data/lib/datadog/profiling/transport/io/client.rb +0 -29
  84. data/lib/datadog/profiling/transport/io/response.rb +0 -18
  85. data/lib/datadog/profiling/transport/io.rb +0 -32
  86. data/lib/datadog/profiling/transport/parcel.rb +0 -19
  87. data/lib/datadog/profiling/transport/request.rb +0 -17
  88. data/lib/datadog/profiling/transport/response.rb +0 -10
  89. data/lib/datadog/tracing/distributed/parser.rb +0 -70
@@ -0,0 +1,341 @@
1
+ #include <ruby.h>
2
+ #include <ruby/thread.h>
3
+ #include <ddprof/ffi.h>
4
+ #include "libddprof_helpers.h"
5
+ #include "ruby_helpers.h"
6
+
7
+ // Used to report profiling data to Datadog.
8
+ // This file implements the native bits of the Datadog::Profiling::HttpTransport class
9
+
10
+ static VALUE ok_symbol = Qnil; // :ok in Ruby
11
+ static VALUE error_symbol = Qnil; // :error in Ruby
12
+
13
+ static ID agentless_id; // id of :agentless in Ruby
14
+ static ID agent_id; // id of :agent in Ruby
15
+
16
+ static ID log_failure_to_process_tag_id; // id of :log_failure_to_process_tag in Ruby
17
+
18
+ static VALUE http_transport_class = Qnil;
19
+
20
+ struct call_exporter_without_gvl_arguments {
21
+ ddprof_ffi_ProfileExporterV3 *exporter;
22
+ ddprof_ffi_Request *request;
23
+ ddprof_ffi_CancellationToken *cancel_token;
24
+ ddprof_ffi_SendResult result;
25
+ bool send_ran;
26
+ };
27
+
28
+ inline static ddprof_ffi_ByteSlice byte_slice_from_ruby_string(VALUE string);
29
+ static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration);
30
+ static ddprof_ffi_NewProfileExporterV3Result create_exporter(VALUE exporter_configuration, VALUE tags_as_array);
31
+ static VALUE handle_exporter_failure(ddprof_ffi_NewProfileExporterV3Result exporter_result);
32
+ static ddprof_ffi_EndpointV3 endpoint_from(VALUE exporter_configuration);
33
+ static ddprof_ffi_Vec_tag convert_tags(VALUE tags_as_array);
34
+ static void safely_log_failure_to_process_tag(ddprof_ffi_Vec_tag tags, VALUE err_details);
35
+ static VALUE _native_do_export(
36
+ VALUE self,
37
+ VALUE exporter_configuration,
38
+ VALUE upload_timeout_milliseconds,
39
+ VALUE start_timespec_seconds,
40
+ VALUE start_timespec_nanoseconds,
41
+ VALUE finish_timespec_seconds,
42
+ VALUE finish_timespec_nanoseconds,
43
+ VALUE pprof_file_name,
44
+ VALUE pprof_data,
45
+ VALUE code_provenance_file_name,
46
+ VALUE code_provenance_data,
47
+ VALUE tags_as_array
48
+ );
49
+ static void *call_exporter_without_gvl(void *call_args);
50
+ static void interrupt_exporter_call(void *cancel_token);
51
+
52
+ void http_transport_init(VALUE profiling_module) {
53
+ http_transport_class = rb_define_class_under(profiling_module, "HttpTransport", rb_cObject);
54
+
55
+ rb_define_singleton_method(http_transport_class, "_native_validate_exporter", _native_validate_exporter, 1);
56
+ rb_define_singleton_method(http_transport_class, "_native_do_export", _native_do_export, 11);
57
+
58
+ ok_symbol = ID2SYM(rb_intern_const("ok"));
59
+ error_symbol = ID2SYM(rb_intern_const("error"));
60
+ agentless_id = rb_intern_const("agentless");
61
+ agent_id = rb_intern_const("agent");
62
+ log_failure_to_process_tag_id = rb_intern_const("log_failure_to_process_tag");
63
+ }
64
+
65
+ inline static ddprof_ffi_ByteSlice byte_slice_from_ruby_string(VALUE string) {
66
+ Check_Type(string, T_STRING);
67
+ ddprof_ffi_ByteSlice byte_slice = {.ptr = (uint8_t *) StringValuePtr(string), .len = RSTRING_LEN(string)};
68
+ return byte_slice;
69
+ }
70
+
71
+ static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration) {
72
+ Check_Type(exporter_configuration, T_ARRAY);
73
+ ddprof_ffi_NewProfileExporterV3Result exporter_result = create_exporter(exporter_configuration, rb_ary_new());
74
+
75
+ VALUE failure_tuple = handle_exporter_failure(exporter_result);
76
+ if (!NIL_P(failure_tuple)) return failure_tuple;
77
+
78
+ // We don't actually need the exporter for now -- we just wanted to validate that we could create it with the
79
+ // settings we were given
80
+ ddprof_ffi_NewProfileExporterV3Result_drop(exporter_result);
81
+
82
+ return rb_ary_new_from_args(2, ok_symbol, Qnil);
83
+ }
84
+
85
+ static ddprof_ffi_NewProfileExporterV3Result create_exporter(VALUE exporter_configuration, VALUE tags_as_array) {
86
+ Check_Type(exporter_configuration, T_ARRAY);
87
+ Check_Type(tags_as_array, T_ARRAY);
88
+
89
+ // This needs to be called BEFORE convert_tags since it can raise an exception and thus cause the ddprof_ffi_Vec_tag
90
+ // to be leaked.
91
+ ddprof_ffi_EndpointV3 endpoint = endpoint_from(exporter_configuration);
92
+
93
+ ddprof_ffi_Vec_tag tags = convert_tags(tags_as_array);
94
+
95
+ ddprof_ffi_NewProfileExporterV3Result exporter_result =
96
+ ddprof_ffi_ProfileExporterV3_new(DDPROF_FFI_CHARSLICE_C("ruby"), &tags, endpoint);
97
+
98
+ ddprof_ffi_Vec_tag_drop(tags);
99
+
100
+ return exporter_result;
101
+ }
102
+
103
+ static VALUE handle_exporter_failure(ddprof_ffi_NewProfileExporterV3Result exporter_result) {
104
+ if (exporter_result.tag == DDPROF_FFI_NEW_PROFILE_EXPORTER_V3_RESULT_OK) return Qnil;
105
+
106
+ VALUE err_details = ruby_string_from_vec_u8(exporter_result.err);
107
+
108
+ ddprof_ffi_NewProfileExporterV3Result_drop(exporter_result);
109
+
110
+ return rb_ary_new_from_args(2, error_symbol, err_details);
111
+ }
112
+
113
+ static ddprof_ffi_EndpointV3 endpoint_from(VALUE exporter_configuration) {
114
+ Check_Type(exporter_configuration, T_ARRAY);
115
+
116
+ ID working_mode = SYM2ID(rb_ary_entry(exporter_configuration, 0)); // SYM2ID verifies its input so we can do this safely
117
+
118
+ if (working_mode != agentless_id && working_mode != agent_id) {
119
+ rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
120
+ }
121
+
122
+ if (working_mode == agentless_id) {
123
+ VALUE site = rb_ary_entry(exporter_configuration, 1);
124
+ VALUE api_key = rb_ary_entry(exporter_configuration, 2);
125
+ Check_Type(site, T_STRING);
126
+ Check_Type(api_key, T_STRING);
127
+
128
+ return ddprof_ffi_EndpointV3_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
129
+ } else { // agent_id
130
+ VALUE base_url = rb_ary_entry(exporter_configuration, 1);
131
+ Check_Type(base_url, T_STRING);
132
+
133
+ return ddprof_ffi_EndpointV3_agent(char_slice_from_ruby_string(base_url));
134
+ }
135
+ }
136
+
137
+ __attribute__((warn_unused_result))
138
+ static ddprof_ffi_Vec_tag convert_tags(VALUE tags_as_array) {
139
+ Check_Type(tags_as_array, T_ARRAY);
140
+
141
+ long tags_count = RARRAY_LEN(tags_as_array);
142
+ ddprof_ffi_Vec_tag tags = ddprof_ffi_Vec_tag_new();
143
+
144
+ for (long i = 0; i < tags_count; i++) {
145
+ VALUE name_value_pair = rb_ary_entry(tags_as_array, i);
146
+
147
+ if (!RB_TYPE_P(name_value_pair, T_ARRAY)) {
148
+ ddprof_ffi_Vec_tag_drop(tags);
149
+ Check_Type(name_value_pair, T_ARRAY);
150
+ }
151
+
152
+ // Note: We can index the array without checking its size first because rb_ary_entry returns Qnil if out of bounds
153
+ VALUE tag_name = rb_ary_entry(name_value_pair, 0);
154
+ VALUE tag_value = rb_ary_entry(name_value_pair, 1);
155
+
156
+ if (!(RB_TYPE_P(tag_name, T_STRING) && RB_TYPE_P(tag_value, T_STRING))) {
157
+ ddprof_ffi_Vec_tag_drop(tags);
158
+ Check_Type(tag_name, T_STRING);
159
+ Check_Type(tag_value, T_STRING);
160
+ }
161
+
162
+ ddprof_ffi_PushTagResult push_result =
163
+ ddprof_ffi_Vec_tag_push(&tags, char_slice_from_ruby_string(tag_name), char_slice_from_ruby_string(tag_value));
164
+
165
+ if (push_result.tag == DDPROF_FFI_PUSH_TAG_RESULT_ERR) {
166
+ VALUE err_details = ruby_string_from_vec_u8(push_result.err);
167
+ ddprof_ffi_PushTagResult_drop(push_result);
168
+
169
+ // libddprof validates tags and may catch invalid tags that ddtrace didn't actually catch.
170
+ // We warn users about such tags, and then just ignore them.
171
+ safely_log_failure_to_process_tag(tags, err_details);
172
+ } else {
173
+ ddprof_ffi_PushTagResult_drop(push_result);
174
+ }
175
+ }
176
+
177
+ return tags;
178
+ }
179
+
180
+ static VALUE log_failure_to_process_tag(VALUE err_details) {
181
+ return rb_funcall(http_transport_class, log_failure_to_process_tag_id, 1, err_details);
182
+ }
183
+
184
+ // Since we are calling into Ruby code, it may raise an exception. This method ensure that dynamically-allocated tags
185
+ // get cleaned before propagating the exception.
186
+ static void safely_log_failure_to_process_tag(ddprof_ffi_Vec_tag tags, VALUE err_details) {
187
+ int exception_state;
188
+ rb_protect(log_failure_to_process_tag, err_details, &exception_state);
189
+
190
+ if (exception_state) { // An exception was raised
191
+ ddprof_ffi_Vec_tag_drop(tags); // clean up
192
+ rb_jump_tag(exception_state); // "Re-raise" exception
193
+ }
194
+ }
195
+
196
+ // Note: This function handles a bunch of libddprof dynamically-allocated objects, so it MUST not use any Ruby APIs
197
+ // which can raise exceptions, otherwise the objects will be leaked.
198
+ static VALUE perform_export(
199
+ ddprof_ffi_NewProfileExporterV3Result valid_exporter_result, // Must be called with a valid exporter result
200
+ ddprof_ffi_Timespec start,
201
+ ddprof_ffi_Timespec finish,
202
+ ddprof_ffi_Slice_file slice_files,
203
+ ddprof_ffi_Vec_tag *additional_tags,
204
+ uint64_t timeout_milliseconds
205
+ ) {
206
+ ddprof_ffi_ProfileExporterV3 *exporter = valid_exporter_result.ok;
207
+ ddprof_ffi_CancellationToken *cancel_token = ddprof_ffi_CancellationToken_new();
208
+ ddprof_ffi_Request *request =
209
+ ddprof_ffi_ProfileExporterV3_build(exporter, start, finish, slice_files, additional_tags, timeout_milliseconds);
210
+
211
+ // We'll release the Global VM Lock while we're calling send, so that the Ruby VM can continue to work while this
212
+ // is pending
213
+ struct call_exporter_without_gvl_arguments args =
214
+ {.exporter = exporter, .request = request, .cancel_token = cancel_token, .send_ran = false};
215
+
216
+ // We use rb_thread_call_without_gvl2 instead of rb_thread_call_without_gvl as the gvl2 variant never raises any
217
+ // exceptions.
218
+ //
219
+ // (With rb_thread_call_without_gvl, if someone calls Thread#kill or something like it on the current thread,
220
+ // the exception will be raised without us being able to clean up dynamically-allocated stuff, which would leak.)
221
+ //
222
+ // Instead, we take care of our own exception checking, and delay the exception raising (`rb_jump_tag` call) until
223
+ // after we cleaned up any dynamically-allocated resources.
224
+ //
225
+ // We run rb_thread_call_without_gvl2 in a loop since an "interrupt" may cause it to return before even running
226
+ // our code. In such a case, we retry the call -- unless the interrupt was caused by an exception being pending,
227
+ // and in that case we also give up and break out of the loop.
228
+ int pending_exception = 0;
229
+
230
+ while (!args.send_ran && !pending_exception) {
231
+ rb_thread_call_without_gvl2(call_exporter_without_gvl, &args, interrupt_exporter_call, cancel_token);
232
+ if (!args.send_ran) pending_exception = check_if_pending_exception();
233
+ }
234
+
235
+ VALUE ruby_status;
236
+ VALUE ruby_result;
237
+
238
+ if (pending_exception) {
239
+ // We're in a weird situation that libddprof doesn't quite support. The ddprof_ffi_Request payload is dynamically
240
+ // allocated and needs to be freed, but libddprof doesn't have an API for dropping a request.
241
+ //
242
+ // There's plans to add a `ddprof_ffi_Request_drop`
243
+ // (https://github.com/DataDog/dd-trace-rb/pull/1923#discussion_r882096221); once that happens, we can use it here.
244
+ //
245
+ // As a workaround, we get libddprof to clean up the request by asking for the send to be cancelled, and then calling
246
+ // it anyway. This will make libddprof free the request and return immediately which gets us the expected effect.
247
+ interrupt_exporter_call((void *) cancel_token);
248
+ call_exporter_without_gvl((void *) &args);
249
+ }
250
+
251
+ ddprof_ffi_SendResult result = args.result;
252
+ bool success = result.tag == DDPROF_FFI_SEND_RESULT_HTTP_RESPONSE;
253
+
254
+ ruby_status = success ? ok_symbol : error_symbol;
255
+ ruby_result = success ? UINT2NUM(result.http_response.code) : ruby_string_from_vec_u8(result.failure);
256
+
257
+ // Clean up all dynamically-allocated things
258
+ ddprof_ffi_SendResult_drop(args.result);
259
+ ddprof_ffi_CancellationToken_drop(cancel_token);
260
+ ddprof_ffi_NewProfileExporterV3Result_drop(valid_exporter_result);
261
+ // The request itself does not need to be freed as libddprof takes care of it.
262
+
263
+ // We've cleaned up everything, so if there's an exception to be raised, let's have it
264
+ if (pending_exception) rb_jump_tag(pending_exception);
265
+
266
+ return rb_ary_new_from_args(2, ruby_status, ruby_result);
267
+ }
268
+
269
+ static VALUE _native_do_export(
270
+ VALUE self,
271
+ VALUE exporter_configuration,
272
+ VALUE upload_timeout_milliseconds,
273
+ VALUE start_timespec_seconds,
274
+ VALUE start_timespec_nanoseconds,
275
+ VALUE finish_timespec_seconds,
276
+ VALUE finish_timespec_nanoseconds,
277
+ VALUE pprof_file_name,
278
+ VALUE pprof_data,
279
+ VALUE code_provenance_file_name,
280
+ VALUE code_provenance_data,
281
+ VALUE tags_as_array
282
+ ) {
283
+ Check_Type(upload_timeout_milliseconds, T_FIXNUM);
284
+ Check_Type(start_timespec_seconds, T_FIXNUM);
285
+ Check_Type(start_timespec_nanoseconds, T_FIXNUM);
286
+ Check_Type(finish_timespec_seconds, T_FIXNUM);
287
+ Check_Type(finish_timespec_nanoseconds, T_FIXNUM);
288
+ Check_Type(pprof_file_name, T_STRING);
289
+ Check_Type(pprof_data, T_STRING);
290
+ Check_Type(code_provenance_file_name, T_STRING);
291
+
292
+ // Code provenance can be disabled and in that case will be set to nil
293
+ bool have_code_provenance = !NIL_P(code_provenance_data);
294
+ if (have_code_provenance) Check_Type(code_provenance_data, T_STRING);
295
+
296
+ uint64_t timeout_milliseconds = NUM2ULONG(upload_timeout_milliseconds);
297
+
298
+ ddprof_ffi_Timespec start =
299
+ {.seconds = NUM2LONG(start_timespec_seconds), .nanoseconds = NUM2UINT(start_timespec_nanoseconds)};
300
+ ddprof_ffi_Timespec finish =
301
+ {.seconds = NUM2LONG(finish_timespec_seconds), .nanoseconds = NUM2UINT(finish_timespec_nanoseconds)};
302
+
303
+ int files_to_report = 1 + (have_code_provenance ? 1 : 0);
304
+ ddprof_ffi_File files[files_to_report];
305
+ ddprof_ffi_Slice_file slice_files = {.ptr = files, .len = files_to_report};
306
+
307
+ files[0] = (ddprof_ffi_File) {
308
+ .name = char_slice_from_ruby_string(pprof_file_name),
309
+ .file = byte_slice_from_ruby_string(pprof_data)
310
+ };
311
+ if (have_code_provenance) {
312
+ files[1] = (ddprof_ffi_File) {
313
+ .name = char_slice_from_ruby_string(code_provenance_file_name),
314
+ .file = byte_slice_from_ruby_string(code_provenance_data)
315
+ };
316
+ }
317
+
318
+ ddprof_ffi_Vec_tag *null_additional_tags = NULL;
319
+
320
+ ddprof_ffi_NewProfileExporterV3Result exporter_result = create_exporter(exporter_configuration, tags_as_array);
321
+ // Note: Do not add anything that can raise exceptions after this line, as otherwise the exporter memory will leak
322
+
323
+ VALUE failure_tuple = handle_exporter_failure(exporter_result);
324
+ if (!NIL_P(failure_tuple)) return failure_tuple;
325
+
326
+ return perform_export(exporter_result, start, finish, slice_files, null_additional_tags, timeout_milliseconds);
327
+ }
328
+
329
+ static void *call_exporter_without_gvl(void *call_args) {
330
+ struct call_exporter_without_gvl_arguments *args = (struct call_exporter_without_gvl_arguments*) call_args;
331
+
332
+ args->result = ddprof_ffi_ProfileExporterV3_send(args->exporter, args->request, args->cancel_token);
333
+ args->send_ran = true;
334
+
335
+ return NULL; // Unused
336
+ }
337
+
338
+ // Called by Ruby when it wants to interrupt call_exporter_without_gvl above, e.g. when the app wants to exit cleanly
339
+ static void interrupt_exporter_call(void *cancel_token) {
340
+ ddprof_ffi_CancellationToken_cancel((ddprof_ffi_CancellationToken *) cancel_token);
341
+ }
@@ -3,15 +3,69 @@
3
3
  # typed: ignore
4
4
 
5
5
  require 'libddprof'
6
+ require 'pathname'
6
7
 
7
8
  module Datadog
8
9
  module Profiling
10
+ # Helpers for extconf.rb
9
11
  module NativeExtensionHelpers
12
+ # Can be set when customers want to skip compiling the native extension entirely
10
13
  ENV_NO_EXTENSION = 'DD_PROFILING_NO_EXTENSION'
14
+ # Can be set to force rubygems to fail gem installation when profiling extension could not be built
15
+ ENV_FAIL_INSTALL_IF_MISSING_EXTENSION = 'DD_PROFILING_FAIL_INSTALL_IF_MISSING_EXTENSION'
11
16
 
12
17
  # Older Rubies don't have the MJIT header, used by the JIT compiler, so we need to use a different approach
13
18
  CAN_USE_MJIT_HEADER = RUBY_VERSION >= '2.6'
14
19
 
20
+ def self.fail_install_if_missing_extension?
21
+ ENV[ENV_FAIL_INSTALL_IF_MISSING_EXTENSION].to_s.strip.downcase == 'true'
22
+ end
23
+
24
+ # Used as an workaround for a limitation with how dynamic linking works in environments where ddtrace and
25
+ # libddprof are moved after the extension gets compiled.
26
+ #
27
+ # Because the libddpprof native library is installed on a non-standard system path, in order for it to be
28
+ # found by the system dynamic linker (e.g. what takes care of dlopen(), which is used to load the profiling
29
+ # native extension), we need to add a "runpath" -- a list of folders to search for libddprof.
30
+ #
31
+ # This runpath gets hardcoded at native library linking time. You can look at it using the `readelf` tool in
32
+ # Linux: e.g. `readelf -d ddtrace_profiling_native_extension.2.7.3_x86_64-linux.so`.
33
+ #
34
+ # In ddtrace 1.1.0, we only set as runpath an absolute path to libddprof. (This gets set automatically by the call
35
+ # to `pkg_config('ddprof_ffi_with_rpath')` in `extconf.rb`). This worked fine as long as libddprof was **NOT**
36
+ # moved from the folder it was present at ddtrace installation/linking time.
37
+ #
38
+ # Unfortunately, environments such as Heroku and AWS Elastic Beanstalk move gems around in the filesystem after
39
+ # installation. Thus, the profiling native extension could not be loaded in these environments
40
+ # (see https://github.com/DataDog/dd-trace-rb/issues/2067) because libddprof could not be found.
41
+ #
42
+ # To workaround this issue, this method computes the **relative** path between the folder where the profiling
43
+ # native extension is going to be installed and the folder where libddprof is installed, and returns it
44
+ # to be set as an additional runpath. (Yes, you can set multiple runpath folders to be searched).
45
+ #
46
+ # This way, if both gems are moved together (and it turns out that they are in these environments),
47
+ # the relative path can still be traversed to find libddprof.
48
+ #
49
+ # This is incredibly awful, and it's kinda bizarre how it's not possible to just find these paths at runtime
50
+ # and set them correctly; rather than needing to set stuff at linking-time and then praying to $deity that
51
+ # weird moves don't happen.
52
+ #
53
+ # As a curiosity, `LD_LIBRARY_PATH` can be used to influence the folders that get searched but **CANNOT BE
54
+ # SET DYNAMICALLY**, e.g. it needs to be set at the start of the process (Ruby VM) and thus it's not something
55
+ # we could setup when doing a `require`.
56
+ #
57
+ def self.libddprof_folder_relative_to_native_lib_folder(
58
+ current_folder: __dir__,
59
+ libddprof_pkgconfig_folder: Libddprof.pkgconfig_folder
60
+ )
61
+ return unless libddprof_pkgconfig_folder
62
+
63
+ profiling_native_lib_folder = "#{current_folder}/../../lib/"
64
+ libddprof_lib_folder = "#{libddprof_pkgconfig_folder}/../"
65
+
66
+ Pathname.new(libddprof_lib_folder).relative_path_from(Pathname.new(profiling_native_lib_folder)).to_s
67
+ end
68
+
15
69
  # Used to check if profiler is supported, including user-visible clear messages explaining why their
16
70
  # system may not be supported.
17
71
  # rubocop:disable Metrics/ModuleLength
@@ -37,15 +91,30 @@ module Datadog
37
91
  end
38
92
 
39
93
  # This banner will show up in the logs/terminal while compiling the native extension
40
- def self.failure_banner_for(reason:, suggested:)
41
- prettify_lines = proc { |lines| lines.map { |line| "| #{line.ljust(76)} |" }.join("\n") }
94
+ def self.failure_banner_for(reason:, suggested:, fail_install:)
95
+ prettify_lines = proc { |lines| Array(lines).map { |line| "| #{line.ljust(76)} |" }.join("\n") }
96
+ outcome =
97
+ if fail_install
98
+ [
99
+ 'Failing installation immediately because the ',
100
+ "`#{ENV_FAIL_INSTALL_IF_MISSING_EXTENSION}` environment variable is set",
101
+ 'to `true`.',
102
+ 'When contacting support, please include the <mkmf.log> file that is shown ',
103
+ 'below.',
104
+ ]
105
+ else
106
+ [
107
+ 'The Datadog Continuous Profiler will not be available,',
108
+ 'but all other ddtrace features will work fine!',
109
+ ]
110
+ end
111
+
42
112
  %(
43
113
  +------------------------------------------------------------------------------+
44
114
  | Could not compile the Datadog Continuous Profiler because |
45
115
  #{prettify_lines.call(reason)}
46
116
  | |
47
- | The Datadog Continuous Profiler will not be available, |
48
- | but all other ddtrace features will work fine! |
117
+ #{prettify_lines.call(outcome)}
49
118
  | |
50
119
  #{prettify_lines.call(suggested)}
51
120
  +------------------------------------------------------------------------------+
@@ -57,6 +126,14 @@ module Datadog
57
126
  [*reason, *suggested].join(' ')
58
127
  end
59
128
 
129
+ # mkmf sets $PKGCONFIG after the `pkg_config` gets used in extconf.rb. When `pkg_config` is unsuccessful, we use
130
+ # this helper to decide if we can show more specific error message vs a generic "something went wrong".
131
+ def self.pkg_config_missing?(command: $PKGCONFIG) # rubocop:disable Style/GlobalVars
132
+ pkg_config_available = command && xsystem("#{command} --version")
133
+
134
+ pkg_config_available != true
135
+ end
136
+
60
137
  CONTACT_SUPPORT = [
61
138
  'For help solving this issue, please contact Datadog support at',
62
139
  '<https://docs.datadoghq.com/help/>.',
@@ -84,6 +161,17 @@ module Datadog
84
161
  suggested: CONTACT_SUPPORT,
85
162
  )
86
163
 
164
+ # Validation for this check is done in extconf.rb because it relies on mkmf
165
+ PKG_CONFIG_IS_MISSING = explain_issue(
166
+ #+-----------------------------------------------------------------------------+
167
+ 'the `pkg-config` system tool is missing.',
168
+ 'This issue can usually be fixed by installing:',
169
+ '1. the `pkg-config` package on Homebrew and Debian/Ubuntu-based Linux;',
170
+ '2. the `pkgconf` package on Arch and Alpine-based Linux;',
171
+ '3. the `pkgconf-pkg-config` package on Fedora/Red Hat-based Linux.',
172
+ suggested: CONTACT_SUPPORT,
173
+ )
174
+
87
175
  private_class_method def self.disabled_via_env?
88
176
  disabled_via_env = explain_issue(
89
177
  'the `DD_PROFILING_NO_EXTENSION` environment variable is/was set to',
@@ -35,7 +35,12 @@ static inline rb_thread_t *thread_struct_from_object(VALUE thread) {
35
35
  }
36
36
 
37
37
  rb_nativethread_id_t pthread_id_for(VALUE thread) {
38
- return thread_struct_from_object(thread)->thread_id;
38
+ // struct rb_native_thread was introduced in Ruby 3.2 (preview2): https://github.com/ruby/ruby/pull/5836
39
+ #ifndef NO_RB_NATIVE_THREAD
40
+ return thread_struct_from_object(thread)->nt->thread_id;
41
+ #else
42
+ return thread_struct_from_object(thread)->thread_id;
43
+ #endif
39
44
  }
40
45
 
41
46
  // Returns the stack depth by using the same approach as rb_profile_frames and backtrace_each: get the positions
@@ -58,6 +63,76 @@ ptrdiff_t stack_depth_for(VALUE thread) {
58
63
  return end_cfp <= cfp ? 0 : end_cfp - cfp - 1;
59
64
  }
60
65
 
66
+ // This was renamed in Ruby 3.2
67
+ #if !defined(ccan_list_for_each) && defined(list_for_each)
68
+ #define ccan_list_for_each list_for_each
69
+ #endif
70
+
71
+ #ifndef USE_LEGACY_LIVING_THREADS_ST // Ruby > 2.1
72
+ // Tries to match rb_thread_list() but that method isn't accessible to extensions
73
+ VALUE ddtrace_thread_list(void) {
74
+ VALUE result = rb_ary_new();
75
+ rb_thread_t *thread = NULL;
76
+
77
+ // Ruby 3 Safety: Our implementation is inspired by `rb_ractor_thread_list` BUT that method wraps the operations below
78
+ // with `RACTOR_LOCK` and `RACTOR_UNLOCK`.
79
+ //
80
+ // This initially made me believe that one MUST grab the ractor lock (which is different from the ractor-scoped Global
81
+ // VM Lock) in able to iterate the `threads.set`. This turned out not to be the case: upon further study of the VM
82
+ // codebase in 3.2-master, 3.1 and 3.0, there's quite a few places where `threads.set` is accessed without grabbing
83
+ // the ractor lock: `ractor_mark` (ractor.c), `thgroup_list` (thread.c), `rb_check_deadlock` (thread.c), etc.
84
+ //
85
+ // I suspect the design in `rb_ractor_thread_list` may be done that way to perhaps in the future expose it to be
86
+ // called from a different Ractor, but I'm not sure...
87
+ #ifdef HAVE_RUBY_RACTOR_H
88
+ rb_ractor_t *current_ractor = GET_RACTOR();
89
+ ccan_list_for_each(&current_ractor->threads.set, thread, lt_node) {
90
+ #else
91
+ rb_vm_t *vm = thread_struct_from_object(rb_thread_current())->vm;
92
+ list_for_each(&vm->living_threads, thread, vmlt_node) {
93
+ #endif
94
+ switch (thread->status) {
95
+ case THREAD_RUNNABLE:
96
+ case THREAD_STOPPED:
97
+ case THREAD_STOPPED_FOREVER:
98
+ rb_ary_push(result, thread->self);
99
+ default:
100
+ break;
101
+ }
102
+ }
103
+
104
+ return result;
105
+ }
106
+ #else // USE_LEGACY_LIVING_THREADS_ST
107
+ static int ddtrace_thread_list_each(st_data_t thread_object, st_data_t _value, void *result_object);
108
+
109
+ // Alternative ddtrace_thread_list implementation for Ruby 2.1. In this Ruby version, living threads were stored in a
110
+ // hashmap (st) instead of a list.
111
+ VALUE ddtrace_thread_list() {
112
+ VALUE result = rb_ary_new();
113
+ st_foreach(thread_struct_from_object(rb_thread_current())->vm->living_threads, ddtrace_thread_list_each, result);
114
+ return result;
115
+ }
116
+
117
+ static int ddtrace_thread_list_each(st_data_t thread_object, st_data_t _value, void *result_object) {
118
+ VALUE result = (VALUE) result_object;
119
+ rb_thread_t *thread = thread_struct_from_object((VALUE) thread_object);
120
+ switch (thread->status) {
121
+ case THREAD_RUNNABLE:
122
+ case THREAD_STOPPED:
123
+ case THREAD_STOPPED_FOREVER:
124
+ rb_ary_push(result, thread->self);
125
+ default:
126
+ break;
127
+ }
128
+ return ST_CONTINUE;
129
+ }
130
+ #endif // USE_LEGACY_LIVING_THREADS_ST
131
+
132
+ bool is_thread_alive(VALUE thread) {
133
+ return thread_struct_from_object(thread)->status != THREAD_KILLED;
134
+ }
135
+
61
136
  // -----------------------------------------------------------------------------
62
137
  // The sources below are modified versions of code extracted from the Ruby project.
63
138
  // Each function is annotated with its origin, why we imported it, and the changes made.
@@ -17,6 +17,9 @@
17
17
 
18
18
  rb_nativethread_id_t pthread_id_for(VALUE thread);
19
19
  ptrdiff_t stack_depth_for(VALUE thread);
20
+ VALUE ddtrace_thread_list(void);
21
+ bool is_thread_alive(VALUE thread);
22
+
20
23
  int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, VALUE *buff, int *lines, bool* is_ruby_frame);
21
24
 
22
25
  // Ruby 3.0 finally added support for showing CFUNC frames (frames for methods written using native code)
@@ -3,7 +3,9 @@
3
3
  #include "clock_id.h"
4
4
 
5
5
  // Each class/module here is implemented in their separate file
6
+ void collectors_cpu_and_wall_time_init(VALUE profiling_module);
6
7
  void collectors_stack_init(VALUE profiling_module);
8
+ void http_transport_init(VALUE profiling_module);
7
9
  void stack_recorder_init(VALUE profiling_module);
8
10
 
9
11
  static VALUE native_working_p(VALUE self);
@@ -20,7 +22,9 @@ void DDTRACE_EXPORT Init_ddtrace_profiling_native_extension(void) {
20
22
 
21
23
  rb_define_singleton_method(native_extension_module, "clock_id_for", clock_id_for, 1); // from clock_id.h
22
24
 
25
+ collectors_cpu_and_wall_time_init(profiling_module);
23
26
  collectors_stack_init(profiling_module);
27
+ http_transport_init(profiling_module);
24
28
  stack_recorder_init(profiling_module);
25
29
  }
26
30
 
@@ -0,0 +1,33 @@
1
+ #pragma once
2
+
3
+ #include <ruby.h>
4
+
5
+ // Processes any pending interruptions, including exceptions to be raised.
6
+ // If there's an exception to be raised, it raises it. In that case, this function does not return.
7
+ static inline VALUE process_pending_interruptions(VALUE _unused) {
8
+ rb_thread_check_ints();
9
+ return Qnil;
10
+ }
11
+
12
+ // Calls process_pending_interruptions BUT "rescues" any exceptions to be raised, returning them instead as
13
+ // a non-zero `pending_exception`.
14
+ //
15
+ // Thus, if there's a non-zero `pending_exception`, the caller MUST call `rb_jump_tag(pending_exception)` after any
16
+ // needed clean-ups.
17
+ //
18
+ // Usage example:
19
+ //
20
+ // ```c
21
+ // foo = ruby_xcalloc(...);
22
+ // pending_exception = check_if_pending_exception();
23
+ // if (pending_exception) {
24
+ // ruby_xfree(foo);
25
+ // rb_jump_tag(pending_exception); // Re-raises exception
26
+ // }
27
+ // ```
28
+ __attribute__((warn_unused_result))
29
+ static inline int check_if_pending_exception(void) {
30
+ int pending_exception;
31
+ rb_protect(process_pending_interruptions, Qnil, &pending_exception);
32
+ return pending_exception;
33
+ }