ddtrace 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
+ }