ddtrace 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -2
- data/ddtrace.gemspec +5 -2
- data/docs/GettingStarted.md +27 -3
- data/docs/ProfilingDevelopment.md +27 -28
- data/docs/UpgradeGuide.md +1 -1
- data/ext/ddtrace_profiling_loader/ddtrace_profiling_loader.c +1 -1
- data/ext/ddtrace_profiling_loader/extconf.rb +1 -0
- data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +6 -5
- data/ext/ddtrace_profiling_native_extension/clock_id.h +1 -1
- data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +1 -1
- data/ext/ddtrace_profiling_native_extension/clock_id_noop.c +1 -1
- data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.c +269 -0
- data/ext/ddtrace_profiling_native_extension/collectors_stack.c +12 -12
- data/ext/ddtrace_profiling_native_extension/collectors_stack.h +9 -0
- data/ext/ddtrace_profiling_native_extension/extconf.rb +44 -3
- data/ext/ddtrace_profiling_native_extension/http_transport.c +341 -0
- data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +92 -4
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +76 -1
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +3 -0
- data/ext/ddtrace_profiling_native_extension/profiling.c +4 -0
- data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +33 -0
- data/ext/ddtrace_profiling_native_extension/stack_recorder.c +18 -10
- data/ext/ddtrace_profiling_native_extension/stack_recorder.h +10 -1
- data/lib/datadog/core/configuration/components.rb +39 -24
- data/lib/datadog/core/configuration/settings.rb +8 -1
- data/lib/datadog/core/environment/platform.rb +40 -0
- data/lib/datadog/core/utils.rb +1 -1
- data/lib/datadog/opentracer/thread_local_scope_manager.rb +26 -3
- data/lib/datadog/profiling/collectors/code_provenance.rb +1 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time.rb +42 -0
- data/lib/datadog/profiling/collectors/stack.rb +2 -0
- data/lib/datadog/profiling/encoding/profile.rb +7 -11
- data/lib/datadog/profiling/exporter.rb +58 -9
- data/lib/datadog/profiling/ext/forking.rb +8 -8
- data/lib/datadog/profiling/ext.rb +2 -15
- data/lib/datadog/profiling/flush.rb +25 -53
- data/lib/datadog/profiling/http_transport.rb +131 -0
- data/lib/datadog/profiling/old_ext.rb +42 -0
- data/lib/datadog/profiling/{recorder.rb → old_recorder.rb} +20 -31
- data/lib/datadog/profiling/scheduler.rb +24 -43
- data/lib/datadog/profiling/transport/http/api/endpoint.rb +9 -31
- data/lib/datadog/profiling/transport/http/client.rb +5 -3
- data/lib/datadog/profiling/transport/http/response.rb +0 -2
- data/lib/datadog/profiling/transport/http.rb +1 -1
- data/lib/datadog/profiling.rb +3 -3
- data/lib/datadog/tracing/context_provider.rb +17 -1
- data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +1 -0
- data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -1
- data/lib/datadog/tracing/contrib/grpc/datadog_interceptor.rb +4 -0
- data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +35 -0
- data/lib/datadog/tracing/contrib/pg/ext.rb +31 -0
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +129 -0
- data/lib/datadog/tracing/contrib/pg/integration.rb +43 -0
- data/lib/datadog/tracing/contrib/pg/patcher.rb +31 -0
- data/lib/datadog/tracing/contrib/rails/configuration/settings.rb +3 -0
- data/lib/datadog/tracing/contrib/rails/framework.rb +2 -1
- data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +1 -0
- data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -1
- data/lib/datadog/tracing/contrib.rb +1 -0
- data/lib/datadog/tracing/distributed/headers/b3.rb +1 -1
- data/lib/datadog/tracing/distributed/headers/b3_single.rb +4 -4
- data/lib/datadog/tracing/distributed/headers/datadog.rb +1 -1
- data/lib/datadog/tracing/distributed/headers/parser.rb +37 -0
- data/lib/datadog/tracing/distributed/helpers.rb +34 -0
- data/lib/datadog/tracing/distributed/metadata/b3.rb +55 -0
- data/lib/datadog/tracing/distributed/metadata/b3_single.rb +66 -0
- data/lib/datadog/tracing/distributed/metadata/datadog.rb +73 -0
- data/lib/datadog/tracing/distributed/metadata/parser.rb +34 -0
- data/lib/datadog/tracing/metadata/ext.rb +25 -0
- data/lib/datadog/tracing/metadata/tagging.rb +6 -0
- data/lib/datadog/tracing/propagation/grpc.rb +65 -55
- data/lib/datadog/tracing/sampling/rate_sampler.rb +2 -2
- data/lib/datadog/tracing/sampling/span/matcher.rb +80 -0
- data/lib/datadog/tracing/span.rb +21 -1
- data/lib/datadog/tracing/span_operation.rb +2 -1
- data/lib/ddtrace/version.rb +1 -1
- metadata +24 -13
- data/lib/datadog/profiling/transport/client.rb +0 -16
- data/lib/datadog/profiling/transport/io/client.rb +0 -29
- data/lib/datadog/profiling/transport/io/response.rb +0 -18
- data/lib/datadog/profiling/transport/io.rb +0 -32
- data/lib/datadog/profiling/transport/parcel.rb +0 -19
- data/lib/datadog/profiling/transport/request.rb +0 -17
- data/lib/datadog/profiling/transport/response.rb +0 -10
- 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
|
-
|
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
|
-
|
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(¤t_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
|
+
}
|