datadog 2.7.0 → 2.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +47 -17
- data/ext/datadog_profiling_native_extension/extconf.rb +3 -8
- data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +3 -9
- data/ext/datadog_profiling_native_extension/profiling.c +6 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -4
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +0 -34
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/component.rb +1 -8
- data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
- data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
- data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
- data/lib/datadog/appsec/event.rb +1 -1
- data/lib/datadog/appsec/processor/context.rb +2 -2
- data/lib/datadog/appsec/remote.rb +1 -3
- data/lib/datadog/appsec/response.rb +7 -11
- data/lib/datadog/appsec.rb +3 -2
- data/lib/datadog/core/configuration/components.rb +17 -1
- data/lib/datadog/core/configuration/settings.rb +10 -0
- data/lib/datadog/core/configuration.rb +9 -1
- data/lib/datadog/core/remote/client/capabilities.rb +6 -0
- data/lib/datadog/core/remote/client.rb +65 -59
- data/lib/datadog/core/telemetry/component.rb +9 -3
- data/lib/datadog/core/telemetry/ext.rb +1 -0
- data/lib/datadog/di/code_tracker.rb +5 -4
- data/lib/datadog/di/component.rb +5 -1
- data/lib/datadog/di/contrib/active_record.rb +1 -0
- data/lib/datadog/di/init.rb +20 -0
- data/lib/datadog/di/instrumenter.rb +81 -11
- data/lib/datadog/di/probe.rb +11 -1
- data/lib/datadog/di/probe_builder.rb +1 -0
- data/lib/datadog/di/probe_manager.rb +4 -1
- data/lib/datadog/di/probe_notification_builder.rb +13 -7
- data/lib/datadog/di/remote.rb +124 -0
- data/lib/datadog/di/serializer.rb +14 -7
- data/lib/datadog/di/transport.rb +2 -1
- data/lib/datadog/di/utils.rb +7 -0
- data/lib/datadog/di.rb +84 -20
- data/lib/datadog/profiling/component.rb +4 -16
- data/lib/datadog/tracing/configuration/settings.rb +4 -8
- data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
- data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
- data/lib/datadog/tracing/tracer.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +3 -0
- metadata +17 -13
- data/lib/datadog/appsec/processor/actions.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f4b811f0c5014e6f325ac55406d225454a2101cc7576df15657ebc74cf47542
|
4
|
+
data.tar.gz: 20c6095b149238c31501bd0be8eb4363a9b22011f750d5f2d68e3f8b730bb970
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af600463b83509c10417cc90fa808b4148baaa0961bde7aa2d1cdea98c6537a68fdde456af2d700a1e8d795a49fbc9aef3f61d68cc81949132abd8d9165ed19e
|
7
|
+
data.tar.gz: 28977b792b9f957e57bf8386b759a59257d0a74c161038ae7f2f4a1b0f8016b8c3828cf71092920c6fd14fd6640e5ab4dbf987dec257fc2ff54061f7342c20bf
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,35 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
|
5
|
+
## [2.8.0] - 2024-12-10
|
6
|
+
|
7
|
+
### Added
|
8
|
+
|
9
|
+
* DI: Dynamic instrumentation is now available in Ruby as a Preview
|
10
|
+
* AppSec: Add SQL injection detection for ActiveRecord for following adapters: `mysql2`, `postgresql`, and `sqlite3` ([#4167][])
|
11
|
+
* Telemetry: Add environment variable to disable logs ([#4153][])
|
12
|
+
* Integrations: Add configuration option `on_error` to Elasticsearch tracing ([#4066][])
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
* Upgrade libdatadog dependency to 14.3.1 ([#4196][])
|
17
|
+
* Profiling: Require Ruby 3.1+ for heap profiling ([#4178][])
|
18
|
+
* AppSec: Update libddwaf to 1.18.0.0.0 ([#4164][])
|
19
|
+
* Single-step: Lower SSI GLIBC requirements down to 2.17 ([#4137][])
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
|
23
|
+
* Integrations: Avoid loading `ActiveSupport::Cache::RedisCacheStore`, which tries to load `redis >= 4.0.1` regardless of the version of Redis the host application has installed ([#4197][])
|
24
|
+
* Profiling: Fix unsafe initialization when using profiler with otel tracing ([#4195][])
|
25
|
+
* Single-step: Add safe NOOP injection script for very old rubies ([#4140][])
|
26
|
+
|
27
|
+
## [2.7.1] - 2024-11-28
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
|
31
|
+
* Tracing: Fix missing version tag ([#4075][])
|
32
|
+
* Profiling: Fix profiling not loading in certain situations on Ruby 2.5 and 3.3 ([#4161][])
|
33
|
+
|
5
34
|
## [2.7.0] - 2024-11-13
|
6
35
|
|
7
36
|
### Added
|
@@ -3028,7 +3057,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
|
|
3028
3057
|
Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
3029
3058
|
|
3030
3059
|
|
3031
|
-
[Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.
|
3060
|
+
[Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.8.0...master
|
3061
|
+
[2.8.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.7.1...v2.8.0
|
3032
3062
|
[2.7.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.6.0...v2.7.0
|
3033
3063
|
[2.6.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.5.0...v2.6.0
|
3034
3064
|
[2.5.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.4.0...v2.5.0
|
@@ -4473,10 +4503,22 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
|
4473
4503
|
[#4027]: https://github.com/DataDog/dd-trace-rb/issues/4027
|
4474
4504
|
[#4033]: https://github.com/DataDog/dd-trace-rb/issues/4033
|
4475
4505
|
[#4065]: https://github.com/DataDog/dd-trace-rb/issues/4065
|
4506
|
+
[#4066]: https://github.com/DataDog/dd-trace-rb/issues/4066
|
4507
|
+
[#4075]: https://github.com/DataDog/dd-trace-rb/issues/4075
|
4476
4508
|
[#4078]: https://github.com/DataDog/dd-trace-rb/issues/4078
|
4477
4509
|
[#4082]: https://github.com/DataDog/dd-trace-rb/issues/4082
|
4478
4510
|
[#4083]: https://github.com/DataDog/dd-trace-rb/issues/4083
|
4479
4511
|
[#4085]: https://github.com/DataDog/dd-trace-rb/issues/4085
|
4512
|
+
[#4137]: https://github.com/DataDog/dd-trace-rb/issues/4137
|
4513
|
+
[#4140]: https://github.com/DataDog/dd-trace-rb/issues/4140
|
4514
|
+
[#4153]: https://github.com/DataDog/dd-trace-rb/issues/4153
|
4515
|
+
[#4161]: https://github.com/DataDog/dd-trace-rb/issues/4161
|
4516
|
+
[#4164]: https://github.com/DataDog/dd-trace-rb/issues/4164
|
4517
|
+
[#4167]: https://github.com/DataDog/dd-trace-rb/issues/4167
|
4518
|
+
[#4178]: https://github.com/DataDog/dd-trace-rb/issues/4178
|
4519
|
+
[#4195]: https://github.com/DataDog/dd-trace-rb/issues/4195
|
4520
|
+
[#4196]: https://github.com/DataDog/dd-trace-rb/issues/4196
|
4521
|
+
[#4197]: https://github.com/DataDog/dd-trace-rb/issues/4197
|
4480
4522
|
[@AdrianLC]: https://github.com/AdrianLC
|
4481
4523
|
[@Azure7111]: https://github.com/Azure7111
|
4482
4524
|
[@BabyGroot]: https://github.com/BabyGroot
|
@@ -226,7 +226,8 @@ static void trigger_sample_for_thread(
|
|
226
226
|
long current_monotonic_wall_time_ns,
|
227
227
|
ddog_CharSlice *ruby_vm_type,
|
228
228
|
ddog_CharSlice *class_name,
|
229
|
-
bool is_gvl_waiting_state
|
229
|
+
bool is_gvl_waiting_state,
|
230
|
+
bool is_safe_to_allocate_objects
|
230
231
|
);
|
231
232
|
static VALUE _native_thread_list(VALUE self);
|
232
233
|
static struct per_thread_context *get_or_create_context_for(VALUE thread, struct thread_context_collector_state *state);
|
@@ -246,7 +247,12 @@ static long cpu_time_now_ns(struct per_thread_context *thread_context);
|
|
246
247
|
static long thread_id_for(VALUE thread);
|
247
248
|
static VALUE _native_stats(VALUE self, VALUE collector_instance);
|
248
249
|
static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
|
249
|
-
static void trace_identifiers_for(
|
250
|
+
static void trace_identifiers_for(
|
251
|
+
struct thread_context_collector_state *state,
|
252
|
+
VALUE thread,
|
253
|
+
struct trace_identifiers *trace_identifiers_result,
|
254
|
+
bool is_safe_to_allocate_objects
|
255
|
+
);
|
250
256
|
static bool should_collect_resource(VALUE root_span);
|
251
257
|
static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
|
252
258
|
static VALUE thread_list(struct thread_context_collector_state *state);
|
@@ -259,7 +265,8 @@ static void ddtrace_otel_trace_identifiers_for(
|
|
259
265
|
VALUE *root_span,
|
260
266
|
VALUE *numeric_span_id,
|
261
267
|
VALUE active_span,
|
262
|
-
VALUE otel_values
|
268
|
+
VALUE otel_values,
|
269
|
+
bool is_safe_to_allocate_objects
|
263
270
|
);
|
264
271
|
static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples);
|
265
272
|
static bool handle_gvl_waiting(
|
@@ -278,7 +285,8 @@ static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSE
|
|
278
285
|
static void otel_without_ddtrace_trace_identifiers_for(
|
279
286
|
struct thread_context_collector_state *state,
|
280
287
|
VALUE thread,
|
281
|
-
struct trace_identifiers *trace_identifiers_result
|
288
|
+
struct trace_identifiers *trace_identifiers_result,
|
289
|
+
bool is_safe_to_allocate_objects
|
282
290
|
);
|
283
291
|
static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
|
284
292
|
static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
|
@@ -647,7 +655,8 @@ static void update_metrics_and_sample(
|
|
647
655
|
current_monotonic_wall_time_ns,
|
648
656
|
NULL,
|
649
657
|
NULL,
|
650
|
-
is_gvl_waiting_state
|
658
|
+
is_gvl_waiting_state,
|
659
|
+
/* is_safe_to_allocate_objects: */ true // We called from a context that's safe to run any regular code, including allocations
|
651
660
|
);
|
652
661
|
}
|
653
662
|
|
@@ -833,7 +842,10 @@ static void trigger_sample_for_thread(
|
|
833
842
|
// These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
|
834
843
|
ddog_CharSlice *ruby_vm_type,
|
835
844
|
ddog_CharSlice *class_name,
|
836
|
-
bool is_gvl_waiting_state
|
845
|
+
bool is_gvl_waiting_state,
|
846
|
+
// If the Ruby VM is at a state that can allocate objects safely, or not. Added for allocation profiling: we're not
|
847
|
+
// allowed to allocate objects (or raise exceptions) when inside the NEWOBJ tracepoint.
|
848
|
+
bool is_safe_to_allocate_objects
|
837
849
|
) {
|
838
850
|
int max_label_count =
|
839
851
|
1 + // thread id
|
@@ -872,11 +884,11 @@ static void trigger_sample_for_thread(
|
|
872
884
|
}
|
873
885
|
|
874
886
|
struct trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
|
875
|
-
trace_identifiers_for(state, thread, &trace_identifiers_result);
|
887
|
+
trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
|
876
888
|
|
877
889
|
if (!trace_identifiers_result.valid && state->otel_context_enabled != OTEL_CONTEXT_ENABLED_FALSE) {
|
878
890
|
// If we couldn't get something with ddtrace, let's see if we can get some trace identifiers from opentelemetry directly
|
879
|
-
otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result);
|
891
|
+
otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
|
880
892
|
}
|
881
893
|
|
882
894
|
if (trace_identifiers_result.valid) {
|
@@ -1289,7 +1301,12 @@ static VALUE _native_gc_tracking(DDTRACE_UNUSED VALUE _self, VALUE collector_ins
|
|
1289
1301
|
}
|
1290
1302
|
|
1291
1303
|
// Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
|
1292
|
-
static void trace_identifiers_for(
|
1304
|
+
static void trace_identifiers_for(
|
1305
|
+
struct thread_context_collector_state *state,
|
1306
|
+
VALUE thread,
|
1307
|
+
struct trace_identifiers *trace_identifiers_result,
|
1308
|
+
bool is_safe_to_allocate_objects
|
1309
|
+
) {
|
1293
1310
|
if (state->otel_context_enabled == OTEL_CONTEXT_ENABLED_ONLY) return;
|
1294
1311
|
if (state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY) return;
|
1295
1312
|
|
@@ -1308,7 +1325,9 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
|
|
1308
1325
|
|
1309
1326
|
VALUE numeric_span_id = Qnil;
|
1310
1327
|
|
1311
|
-
if (otel_values != Qnil)
|
1328
|
+
if (otel_values != Qnil) {
|
1329
|
+
ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values, is_safe_to_allocate_objects);
|
1330
|
+
}
|
1312
1331
|
|
1313
1332
|
if (root_span == Qnil || (active_span == Qnil && numeric_span_id == Qnil)) return;
|
1314
1333
|
|
@@ -1474,7 +1493,8 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
|
|
1474
1493
|
INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
|
1475
1494
|
&ruby_vm_type,
|
1476
1495
|
optional_class_name,
|
1477
|
-
false
|
1496
|
+
/* is_gvl_waiting_state: */ false,
|
1497
|
+
/* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
|
1478
1498
|
);
|
1479
1499
|
}
|
1480
1500
|
|
@@ -1529,11 +1549,18 @@ static VALUE read_otel_current_span_key_const(DDTRACE_UNUSED VALUE _unused) {
|
|
1529
1549
|
return rb_const_get(trace_module, rb_intern("CURRENT_SPAN_KEY"));
|
1530
1550
|
}
|
1531
1551
|
|
1532
|
-
static VALUE get_otel_current_span_key(struct thread_context_collector_state *state) {
|
1552
|
+
static VALUE get_otel_current_span_key(struct thread_context_collector_state *state, bool is_safe_to_allocate_objects) {
|
1533
1553
|
if (state->otel_current_span_key == Qtrue) { // Qtrue means we haven't tried to extract it yet
|
1554
|
+
if (!is_safe_to_allocate_objects) {
|
1555
|
+
// Calling read_otel_current_span_key_const below can trigger exceptions and arbitrary Ruby code running (e.g.
|
1556
|
+
// `const_missing`, etc). Not safe to call in this situation, so we just skip otel info for this sample.
|
1557
|
+
return Qnil;
|
1558
|
+
}
|
1559
|
+
|
1534
1560
|
// If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
|
1535
1561
|
// gets refactored, we should not fall on our face)
|
1536
1562
|
VALUE span_key = rb_protect(read_otel_current_span_key_const, Qnil, NULL);
|
1563
|
+
rb_set_errinfo(Qnil); // **Clear any pending exception after ignoring it**
|
1537
1564
|
|
1538
1565
|
// Note that this gets set to Qnil if we failed to extract the correct value, and thus we won't try to extract it again
|
1539
1566
|
state->otel_current_span_key = span_key;
|
@@ -1550,7 +1577,8 @@ static void ddtrace_otel_trace_identifiers_for(
|
|
1550
1577
|
VALUE *root_span,
|
1551
1578
|
VALUE *numeric_span_id,
|
1552
1579
|
VALUE active_span,
|
1553
|
-
VALUE otel_values
|
1580
|
+
VALUE otel_values,
|
1581
|
+
bool is_safe_to_allocate_objects
|
1554
1582
|
) {
|
1555
1583
|
VALUE resolved_numeric_span_id =
|
1556
1584
|
active_span == Qnil ?
|
@@ -1561,7 +1589,7 @@ static void ddtrace_otel_trace_identifiers_for(
|
|
1561
1589
|
|
1562
1590
|
if (resolved_numeric_span_id == Qnil) return;
|
1563
1591
|
|
1564
|
-
VALUE otel_current_span_key = get_otel_current_span_key(state);
|
1592
|
+
VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
|
1565
1593
|
if (otel_current_span_key == Qnil) return;
|
1566
1594
|
VALUE current_trace = *active_trace;
|
1567
1595
|
|
@@ -1640,14 +1668,15 @@ static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self
|
|
1640
1668
|
static void otel_without_ddtrace_trace_identifiers_for(
|
1641
1669
|
struct thread_context_collector_state *state,
|
1642
1670
|
VALUE thread,
|
1643
|
-
struct trace_identifiers *trace_identifiers_result
|
1671
|
+
struct trace_identifiers *trace_identifiers_result,
|
1672
|
+
bool is_safe_to_allocate_objects
|
1644
1673
|
) {
|
1645
1674
|
VALUE context_storage = rb_thread_local_aref(thread, otel_context_storage_id /* __opentelemetry_context_storage__ */);
|
1646
1675
|
|
1647
1676
|
// If it exists, context_storage is expected to be an Array[OpenTelemetry::Context]
|
1648
1677
|
if (context_storage == Qnil || !RB_TYPE_P(context_storage, T_ARRAY)) return;
|
1649
1678
|
|
1650
|
-
VALUE otel_current_span_key = get_otel_current_span_key(state);
|
1679
|
+
VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
|
1651
1680
|
if (otel_current_span_key == Qnil) return;
|
1652
1681
|
|
1653
1682
|
int active_context_index = RARRAY_LEN(context_storage) - 1;
|
@@ -1939,7 +1968,8 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
|
|
1939
1968
|
gvl_waiting_started_wall_time_ns,
|
1940
1969
|
NULL,
|
1941
1970
|
NULL,
|
1942
|
-
false // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
|
1971
|
+
/* is_gvl_waiting_state: */ false, // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
|
1972
|
+
/* is_safe_to_allocate_objects: */ true // This is similar to a regular cpu/wall sample, so it's also safe
|
1943
1973
|
);
|
1944
1974
|
}
|
1945
1975
|
|
@@ -131,6 +131,9 @@ end
|
|
131
131
|
|
132
132
|
have_func "malloc_stats"
|
133
133
|
|
134
|
+
# On Ruby 2.5 and 3.3, this symbol was not visible. It is on 2.6 to 3.2, as well as 3.4+
|
135
|
+
$defs << "-DNO_RB_OBJ_INFO" if RUBY_VERSION.start_with?("2.5", "3.3")
|
136
|
+
|
134
137
|
# On older Rubies, rb_postponed_job_preregister/rb_postponed_job_trigger did not exist
|
135
138
|
$defs << "-DNO_POSTPONED_TRIGGER" if RUBY_VERSION < "3.3"
|
136
139
|
|
@@ -167,11 +170,6 @@ $defs << "-DNO_THREAD_TID" if RUBY_VERSION < "3.1"
|
|
167
170
|
# On older Rubies, there was no jit_return member on the rb_control_frame_t struct
|
168
171
|
$defs << "-DNO_JIT_RETURN" if RUBY_VERSION < "3.1"
|
169
172
|
|
170
|
-
# On older Rubies, rb_gc_force_recycle allowed to free objects in a way that
|
171
|
-
# would be invisible to free tracepoints, finalizers and without cleaning
|
172
|
-
# obj_to_id_tbl mappings.
|
173
|
-
$defs << "-DHAVE_WORKING_RB_GC_FORCE_RECYCLE" if RUBY_VERSION < "3.1"
|
174
|
-
|
175
173
|
# On older Rubies, there are no Ractors
|
176
174
|
$defs << "-DNO_RACTORS" if RUBY_VERSION < "3"
|
177
175
|
|
@@ -181,9 +179,6 @@ $defs << "-DNO_IMEMO_NAME" if RUBY_VERSION < "3"
|
|
181
179
|
# On older Rubies, objects would not move
|
182
180
|
$defs << "-DNO_T_MOVED" if RUBY_VERSION < "2.7"
|
183
181
|
|
184
|
-
# On older Rubies, there was no RUBY_SEEN_OBJ_ID flag
|
185
|
-
$defs << "-DNO_SEEN_OBJ_ID_FLAG" if RUBY_VERSION < "2.7"
|
186
|
-
|
187
182
|
# On older Rubies, rb_global_vm_lock_struct did not include the owner field
|
188
183
|
$defs << "-DNO_GVL_OWNER" if RUBY_VERSION < "2.6"
|
189
184
|
|
@@ -7,10 +7,6 @@
|
|
7
7
|
#include "libdatadog_helpers.h"
|
8
8
|
#include "time_helpers.h"
|
9
9
|
|
10
|
-
#if (defined(HAVE_WORKING_RB_GC_FORCE_RECYCLE) && ! defined(NO_SEEN_OBJ_ID_FLAG))
|
11
|
-
#define CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
12
|
-
#endif
|
13
|
-
|
14
10
|
// Minimum age (in GC generations) of heap objects we want to include in heap
|
15
11
|
// recorder iterations. Object with age 0 represent objects that have yet to undergo
|
16
12
|
// a GC and, thus, may just be noise/trash at instant of iteration and are usually not
|
@@ -123,9 +119,6 @@ typedef struct {
|
|
123
119
|
// Pointer to the (potentially partial) object_record containing metadata about an ongoing recording.
|
124
120
|
// When NULL, this symbolizes an unstarted/invalid recording.
|
125
121
|
object_record *object_record;
|
126
|
-
// A flag to track whether we had to force set the RUBY_FL_SEEN_OBJ_ID flag on this object
|
127
|
-
// as part of our workaround around rb_gc_force_recycle issues.
|
128
|
-
bool did_recycle_workaround;
|
129
122
|
} recording;
|
130
123
|
|
131
124
|
struct heap_recorder {
|
@@ -342,46 +335,12 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
|
|
342
335
|
rb_raise(rb_eRuntimeError, "Detected a bignum object id. These are not supported by heap profiling.");
|
343
336
|
}
|
344
337
|
|
345
|
-
bool did_recycle_workaround = false;
|
346
|
-
|
347
|
-
#ifdef CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
348
|
-
// If we are in a ruby version that has a working rb_gc_force_recycle implementation,
|
349
|
-
// its usage may lead to an object being re-used outside of the typical GC cycle.
|
350
|
-
//
|
351
|
-
// This re-use is in theory invisible to us unless we're lucky enough to sample both
|
352
|
-
// the original object and the replacement that uses the recycled slot.
|
353
|
-
//
|
354
|
-
// In practice, we've observed (https://github.com/DataDog/dd-trace-rb/pull/3366)
|
355
|
-
// that non-noop implementations of rb_gc_force_recycle have an implementation bug
|
356
|
-
// which results in the object that re-used the recycled slot inheriting the same
|
357
|
-
// object id without setting the FL_SEEN_OBJ_ID flag. We rely on this knowledge to
|
358
|
-
// "observe" implicit frees when an object we are tracking is force-recycled.
|
359
|
-
//
|
360
|
-
// However, it may happen that we start tracking a new object and that object was
|
361
|
-
// allocated on a recycled slot. Due to the bug, this object would be missing the
|
362
|
-
// FL_SEEN_OBJ_ID flag even though it was not recycled itself. If we left it be,
|
363
|
-
// when we're doing our liveness check, the absence of the flag would trigger our
|
364
|
-
// implicit free workaround and the object would be inferred as recycled even though
|
365
|
-
// it might still be alive.
|
366
|
-
//
|
367
|
-
// Thus, if we detect that this new allocation is already missing the flag at the start
|
368
|
-
// of the heap allocation recording, we force-set it. This should be safe since we
|
369
|
-
// just called rb_obj_id on it above and the expectation is that any flaggable object
|
370
|
-
// that goes through it ends up with the flag set (as evidenced by the GC_ASSERT
|
371
|
-
// lines in https://github.com/ruby/ruby/blob/4a8d7246d15b2054eacb20f8ab3d29d39a3e7856/gc.c#L4050C14-L4050C14).
|
372
|
-
if (RB_FL_ABLE(new_obj) && !RB_FL_TEST(new_obj, RUBY_FL_SEEN_OBJ_ID)) {
|
373
|
-
RB_FL_SET(new_obj, RUBY_FL_SEEN_OBJ_ID);
|
374
|
-
did_recycle_workaround = true;
|
375
|
-
}
|
376
|
-
#endif
|
377
|
-
|
378
338
|
heap_recorder->active_recording = (recording) {
|
379
339
|
.object_record = object_record_new(FIX2LONG(ruby_obj_id), NULL, (live_object_data) {
|
380
340
|
.weight = weight * heap_recorder->sample_rate,
|
381
341
|
.class = alloc_class != NULL ? string_from_char_slice(*alloc_class) : NULL,
|
382
342
|
.alloc_gen = rb_gc_count(),
|
383
|
-
|
384
|
-
.did_recycle_workaround = did_recycle_workaround,
|
343
|
+
}),
|
385
344
|
};
|
386
345
|
}
|
387
346
|
|
@@ -685,41 +644,6 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
|
|
685
644
|
|
686
645
|
// If we got this far, then we found a valid live object for the tracked id.
|
687
646
|
|
688
|
-
#ifdef CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
689
|
-
// If we are in a ruby version that has a working rb_gc_force_recycle implementation,
|
690
|
-
// its usage may lead to an object being re-used outside of the typical GC cycle.
|
691
|
-
//
|
692
|
-
// This re-use is in theory invisible to us and would mean that the ref from which we
|
693
|
-
// collected the object_record metadata may not be the same as the current ref and
|
694
|
-
// thus any further reporting would be innacurately attributed to stale metadata.
|
695
|
-
//
|
696
|
-
// In practice, there is a way for us to notice that this happened because of a bug
|
697
|
-
// in the implementation of rb_gc_force_recycle. Our heap profiler relies on object
|
698
|
-
// ids and id2ref to detect whether objects are still alive. Turns out that when an
|
699
|
-
// object with an id is re-used via rb_gc_force_recycle, it will "inherit" the ID
|
700
|
-
// of the old object but it will NOT have the FL_SEEN_OBJ_ID as per the experiment
|
701
|
-
// in https://github.com/DataDog/dd-trace-rb/pull/3360#discussion_r1442823517
|
702
|
-
//
|
703
|
-
// Thus, if we detect that the ref we just resolved above is missing this flag, we can
|
704
|
-
// safely say re-use happened and thus treat it as an implicit free of the object
|
705
|
-
// we were tracking (the original one which got recycled).
|
706
|
-
if (RB_FL_ABLE(ref) && !RB_FL_TEST(ref, RUBY_FL_SEEN_OBJ_ID)) {
|
707
|
-
|
708
|
-
// NOTE: We don't really need to set this flag for heap recorder to work correctly
|
709
|
-
// but doing so partially mitigates a bug in runtimes with working rb_gc_force_recycle
|
710
|
-
// which leads to broken invariants and leaking of entries in obj_to_id and id_to_obj
|
711
|
-
// tables in objspace. We already do the same thing when we sample a recycled object,
|
712
|
-
// here we apply it as well to objects that replace recycled objects that were being
|
713
|
-
// tracked. More details in https://github.com/DataDog/dd-trace-rb/pull/3366
|
714
|
-
RB_FL_SET(ref, RUBY_FL_SEEN_OBJ_ID);
|
715
|
-
|
716
|
-
on_committed_object_record_cleanup(recorder, record);
|
717
|
-
recorder->stats_last_update.objects_dead++;
|
718
|
-
return ST_DELETE;
|
719
|
-
}
|
720
|
-
|
721
|
-
#endif
|
722
|
-
|
723
647
|
if (
|
724
648
|
recorder->size_enabled &&
|
725
649
|
recorder->update_include_old && // We only update sizes when doing a full update
|
@@ -732,6 +656,10 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
|
|
732
656
|
record->object_data.is_frozen = RB_OBJ_FROZEN(ref);
|
733
657
|
}
|
734
658
|
|
659
|
+
// Ensure that ref is kept on the stack so the Ruby garbage collector does not try to clean up the object before this
|
660
|
+
// point.
|
661
|
+
RB_GC_GUARD(ref);
|
662
|
+
|
735
663
|
recorder->stats_last_update.objects_alive++;
|
736
664
|
if (record->object_data.is_frozen) {
|
737
665
|
recorder->stats_last_update.objects_frozen++;
|
@@ -803,18 +731,12 @@ static int update_object_record_entry(DDTRACE_UNUSED st_data_t *key, st_data_t *
|
|
803
731
|
object_record *new_object_record = recording.object_record;
|
804
732
|
if (existing) {
|
805
733
|
object_record *existing_record = (object_record*) (*value);
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
// This is not supposed to happen, raising...
|
813
|
-
VALUE existing_inspect = object_record_inspect(existing_record);
|
814
|
-
VALUE new_inspect = object_record_inspect(new_object_record);
|
815
|
-
rb_raise(rb_eRuntimeError, "Object ids are supposed to be unique. We got 2 allocation recordings with "
|
816
|
-
"the same id. previous=%"PRIsVALUE" new=%"PRIsVALUE, existing_inspect, new_inspect);
|
817
|
-
}
|
734
|
+
|
735
|
+
// This is not supposed to happen, raising...
|
736
|
+
VALUE existing_inspect = object_record_inspect(existing_record);
|
737
|
+
VALUE new_inspect = object_record_inspect(new_object_record);
|
738
|
+
rb_raise(rb_eRuntimeError, "Object ids are supposed to be unique. We got 2 allocation recordings with "
|
739
|
+
"the same id. previous=%"PRIsVALUE" new=%"PRIsVALUE, existing_inspect, new_inspect);
|
818
740
|
}
|
819
741
|
// Always carry on with the update, we want the new record to be there at the end
|
820
742
|
(*value) = (st_data_t) new_object_record;
|
@@ -158,7 +158,7 @@ bool is_current_thread_holding_the_gvl(void) {
|
|
158
158
|
//
|
159
159
|
// Thus an incorrect `is_current_thread_holding_the_gvl` result may lead to issues inside `rb_postponed_job_register_one`.
|
160
160
|
//
|
161
|
-
// For this reason we
|
161
|
+
// For this reason we default to use the "no signals workaround" on Ruby 2.5 by default, and we print a
|
162
162
|
// warning when customers force-enable it.
|
163
163
|
bool gvl_acquired = vm->gvl.acquired != 0;
|
164
164
|
rb_thread_t *current_owner = vm->running_thread;
|
@@ -587,16 +587,13 @@ int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, frame_info *st
|
|
587
587
|
// Taken from upstream vm_insnhelper.c at commit 5f10bd634fb6ae8f74a4ea730176233b0ca96954 (March 2022, Ruby 3.2 trunk)
|
588
588
|
// Copyright (C) 2007 Koichi Sasada
|
589
589
|
// to support our custom rb_profile_frames (see above)
|
590
|
-
// Modifications:
|
590
|
+
// Modifications:
|
591
|
+
// * Removed debug checks (they were ifdef'd out anyway)
|
591
592
|
static rb_callable_method_entry_t *
|
592
593
|
check_method_entry(VALUE obj, int can_be_svar)
|
593
594
|
{
|
594
595
|
if (obj == Qfalse) return NULL;
|
595
596
|
|
596
|
-
#if VM_CHECK_MODE > 0
|
597
|
-
if (!RB_TYPE_P(obj, T_IMEMO)) rb_bug("check_method_entry: unknown type: %s", rb_obj_info(obj));
|
598
|
-
#endif
|
599
|
-
|
600
597
|
switch (imemo_type(obj)) {
|
601
598
|
case imemo_ment:
|
602
599
|
return (rb_callable_method_entry_t *)obj;
|
@@ -608,9 +605,6 @@ check_method_entry(VALUE obj, int can_be_svar)
|
|
608
605
|
}
|
609
606
|
// fallthrough
|
610
607
|
default:
|
611
|
-
#if VM_CHECK_MODE > 0
|
612
|
-
rb_bug("check_method_entry: svar should not be there:");
|
613
|
-
#endif
|
614
608
|
return NULL;
|
615
609
|
}
|
616
610
|
}
|
@@ -37,6 +37,7 @@ static VALUE _native_trigger_holding_the_gvl_signal_handler_on(DDTRACE_UNUSED VA
|
|
37
37
|
static VALUE _native_enforce_success(DDTRACE_UNUSED VALUE _self, VALUE syserr_errno, VALUE with_gvl);
|
38
38
|
static void *trigger_enforce_success(void *trigger_args);
|
39
39
|
static VALUE _native_malloc_stats(DDTRACE_UNUSED VALUE _self);
|
40
|
+
static VALUE _native_safe_object_info(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
40
41
|
|
41
42
|
void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
|
42
43
|
VALUE datadog_module = rb_define_module("Datadog");
|
@@ -72,6 +73,7 @@ void DDTRACE_EXPORT Init_datadog_profiling_native_extension(void) {
|
|
72
73
|
rb_define_singleton_method(testing_module, "_native_trigger_holding_the_gvl_signal_handler_on", _native_trigger_holding_the_gvl_signal_handler_on, 1);
|
73
74
|
rb_define_singleton_method(testing_module, "_native_enforce_success", _native_enforce_success, 2);
|
74
75
|
rb_define_singleton_method(testing_module, "_native_malloc_stats", _native_malloc_stats, 0);
|
76
|
+
rb_define_singleton_method(testing_module, "_native_safe_object_info", _native_safe_object_info, 1);
|
75
77
|
}
|
76
78
|
|
77
79
|
static VALUE native_working_p(DDTRACE_UNUSED VALUE _self) {
|
@@ -265,3 +267,7 @@ static VALUE _native_malloc_stats(DDTRACE_UNUSED VALUE _self) {
|
|
265
267
|
return Qfalse;
|
266
268
|
#endif
|
267
269
|
}
|
270
|
+
|
271
|
+
static VALUE _native_safe_object_info(DDTRACE_UNUSED VALUE _self, VALUE obj) {
|
272
|
+
return rb_str_new_cstr(safe_object_info(obj));
|
273
|
+
}
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
#include "ruby_helpers.h"
|
5
5
|
#include "private_vm_api_access.h"
|
6
|
+
#include "extconf.h"
|
6
7
|
|
7
8
|
// The following global variables are initialized at startup to save expensive lookups later.
|
8
9
|
// They are not expected to be mutated outside of init.
|
@@ -219,17 +220,26 @@ static bool ruby_is_obj_with_class(VALUE obj) {
|
|
219
220
|
return false;
|
220
221
|
}
|
221
222
|
|
222
|
-
//
|
223
|
+
// This function is not present in the VM headers, but is a public symbol that can be invoked.
|
223
224
|
int rb_objspace_internal_object_p(VALUE obj);
|
224
|
-
|
225
|
+
|
226
|
+
#ifdef NO_RB_OBJ_INFO
|
227
|
+
const char* safe_object_info(DDTRACE_UNUSED VALUE obj) { return "(No rb_obj_info for current Ruby)"; }
|
228
|
+
#else
|
229
|
+
// This function is a public symbol, but not on all Rubies; `safe_object_info` below abstracts this, and
|
230
|
+
// should be used instead.
|
231
|
+
const char *rb_obj_info(VALUE obj);
|
232
|
+
|
233
|
+
const char* safe_object_info(VALUE obj) { return rb_obj_info(obj); }
|
234
|
+
#endif
|
225
235
|
|
226
236
|
VALUE ruby_safe_inspect(VALUE obj) {
|
227
237
|
if (!ruby_is_obj_with_class(obj)) return rb_str_new_cstr("(Not an object)");
|
228
|
-
if (rb_objspace_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)",
|
238
|
+
if (rb_objspace_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)", safe_object_info(obj));
|
229
239
|
// @ivoanjo: I saw crashes on Ruby 3.1.4 when trying to #inspect matchdata objects. I'm not entirely sure why this
|
230
240
|
// is needed, but since we only use this method for debug purposes I put in this alternative and decided not to
|
231
241
|
// dig deeper.
|
232
|
-
if (rb_type(obj) == RUBY_T_MATCH) return rb_sprintf("(MatchData, %s)",
|
242
|
+
if (rb_type(obj) == RUBY_T_MATCH) return rb_sprintf("(MatchData, %s)", safe_object_info(obj));
|
233
243
|
if (rb_respond_to(obj, inspect_id)) return rb_sprintf("%+"PRIsVALUE, obj);
|
234
244
|
if (rb_respond_to(obj, to_s_id)) return rb_sprintf("%"PRIsVALUE, obj);
|
235
245
|
|
@@ -90,3 +90,7 @@ size_t ruby_obj_memsize_of(VALUE obj);
|
|
90
90
|
// return a string with the result of that call. Elsif the object responds to
|
91
91
|
// 'to_s', return a string with the result of that call. Otherwise, return Qnil.
|
92
92
|
VALUE ruby_safe_inspect(VALUE obj);
|
93
|
+
|
94
|
+
// You probably want ruby_safe_inspect instead; this is a lower-level dependency
|
95
|
+
// of it, that's being exposed here just to facilitate testing.
|
96
|
+
const char* safe_object_info(VALUE obj);
|
@@ -258,8 +258,6 @@ static VALUE _native_check_heap_hashes(DDTRACE_UNUSED VALUE _self, VALUE locatio
|
|
258
258
|
static VALUE _native_start_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
259
259
|
static VALUE _native_end_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
260
260
|
static VALUE _native_debug_heap_recorder(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
261
|
-
static VALUE _native_gc_force_recycle(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
262
|
-
static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
263
261
|
static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance);
|
264
262
|
static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns, long heap_iteration_prep_time_ns, long heap_profile_build_time_ns);
|
265
263
|
static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE object_id);
|
@@ -297,10 +295,6 @@ void stack_recorder_init(VALUE profiling_module) {
|
|
297
295
|
_native_end_fake_slow_heap_serialization, 1);
|
298
296
|
rb_define_singleton_method(testing_module, "_native_debug_heap_recorder",
|
299
297
|
_native_debug_heap_recorder, 1);
|
300
|
-
rb_define_singleton_method(testing_module, "_native_gc_force_recycle",
|
301
|
-
_native_gc_force_recycle, 1);
|
302
|
-
rb_define_singleton_method(testing_module, "_native_has_seen_id_flag",
|
303
|
-
_native_has_seen_id_flag, 1);
|
304
298
|
rb_define_singleton_method(testing_module, "_native_is_object_recorded?", _native_is_object_recorded, 2);
|
305
299
|
rb_define_singleton_method(testing_module, "_native_heap_recorder_reset_last_update", _native_heap_recorder_reset_last_update, 1);
|
306
300
|
rb_define_singleton_method(testing_module, "_native_recorder_after_gc_step", _native_recorder_after_gc_step, 1);
|
@@ -1006,34 +1000,6 @@ static VALUE _native_debug_heap_recorder(DDTRACE_UNUSED VALUE _self, VALUE recor
|
|
1006
1000
|
return heap_recorder_testonly_debug(state->heap_recorder);
|
1007
1001
|
}
|
1008
1002
|
|
1009
|
-
#pragma GCC diagnostic push
|
1010
|
-
// rb_gc_force_recycle was deprecated in latest versions of Ruby and is a noop.
|
1011
|
-
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
1012
|
-
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
1013
|
-
// This method exists only to enable testing Datadog::Profiling::StackRecorder behavior using RSpec.
|
1014
|
-
// It SHOULD NOT be used for other purposes.
|
1015
|
-
static VALUE _native_gc_force_recycle(DDTRACE_UNUSED VALUE _self, VALUE obj) {
|
1016
|
-
#ifdef HAVE_WORKING_RB_GC_FORCE_RECYCLE
|
1017
|
-
rb_gc_force_recycle(obj);
|
1018
|
-
#endif
|
1019
|
-
return Qnil;
|
1020
|
-
}
|
1021
|
-
#pragma GCC diagnostic pop
|
1022
|
-
|
1023
|
-
// This method exists only to enable testing Datadog::Profiling::StackRecorder behavior using RSpec.
|
1024
|
-
// It SHOULD NOT be used for other purposes.
|
1025
|
-
static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj) {
|
1026
|
-
#ifndef NO_SEEN_OBJ_ID_FLAG
|
1027
|
-
if (RB_FL_TEST(obj, RUBY_FL_SEEN_OBJ_ID)) {
|
1028
|
-
return Qtrue;
|
1029
|
-
} else {
|
1030
|
-
return Qfalse;
|
1031
|
-
}
|
1032
|
-
#else
|
1033
|
-
return Qfalse;
|
1034
|
-
#endif
|
1035
|
-
}
|
1036
|
-
|
1037
1003
|
static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE recorder_instance) {
|
1038
1004
|
struct stack_recorder_state *state;
|
1039
1005
|
TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
|
@@ -8,7 +8,7 @@ module Datadog
|
|
8
8
|
module LibdatadogExtconfHelpers
|
9
9
|
# Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
|
10
10
|
# may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
|
11
|
-
LIBDATADOG_VERSION = '~> 14.1.
|
11
|
+
LIBDATADOG_VERSION = '~> 14.3.1.1.0'
|
12
12
|
|
13
13
|
# Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
|
14
14
|
# libdatadog are moved after the extension gets compiled.
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require_relative 'processor'
|
4
4
|
require_relative 'processor/rule_merger'
|
5
5
|
require_relative 'processor/rule_loader'
|
6
|
-
require_relative 'processor/actions'
|
7
6
|
|
8
7
|
module Datadog
|
9
8
|
module AppSec
|
@@ -52,10 +51,6 @@ module Datadog
|
|
52
51
|
)
|
53
52
|
return nil unless rules
|
54
53
|
|
55
|
-
actions = rules['actions']
|
56
|
-
|
57
|
-
AppSec::Processor::Actions.merge(actions) if actions
|
58
|
-
|
59
54
|
data = AppSec::Processor::RuleLoader.load_data(
|
60
55
|
ip_denylist: settings.appsec.ip_denylist,
|
61
56
|
user_id_denylist: settings.appsec.user_id_denylist,
|
@@ -84,10 +79,8 @@ module Datadog
|
|
84
79
|
@mutex = Mutex.new
|
85
80
|
end
|
86
81
|
|
87
|
-
def reconfigure(ruleset:,
|
82
|
+
def reconfigure(ruleset:, telemetry:)
|
88
83
|
@mutex.synchronize do
|
89
|
-
AppSec::Processor::Actions.merge(actions)
|
90
|
-
|
91
84
|
new = Processor.new(ruleset: ruleset, telemetry: telemetry)
|
92
85
|
|
93
86
|
if new && new.ready?
|