Skip to content

Commit bef15b9

Browse files
p-datadogvpellan
authored andcommitted
DI: report evaluation errors in the correct schema (#4951)
1 parent ffaf7d9 commit bef15b9

File tree

12 files changed

+157
-25
lines changed

12 files changed

+157
-25
lines changed

lib/datadog/di/el/compiler.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module EL
1717
# @api private
1818
class Compiler
1919
def compile(ast)
20-
Expression.new(compile_partial(ast))
20+
compile_partial(ast)
2121
end
2222

2323
private

lib/datadog/di/el/expression.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ module EL
77
#
88
# @api private
99
class Expression
10-
def initialize(compiled_expr)
10+
def initialize(dsl_expr, compiled_expr)
1111
unless String === compiled_expr
1212
raise ArgumentError, "compiled_expr must be a string"
1313
end
1414

15+
@dsl_expr = dsl_expr
16+
1517
cls = Class.new(Evaluator)
1618
cls.class_exec do
1719
eval(<<-RUBY, Object.new.send(:binding), __FILE__, __LINE__ + 1) # standard:disable Security/Eval
@@ -24,6 +26,7 @@ def evaluate(context)
2426
@evaluator = cls.new
2527
end
2628

29+
attr_reader :dsl_expr
2730
attr_reader :evaluator
2831

2932
def evaluate(context)

lib/datadog/di/probe_builder.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ def build_from_remote_config(config)
3030
# The validations here are not yet comprehensive.
3131
type = config.fetch('type')
3232
type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
33+
cond = if cond_spec = config['when']
34+
unless cond_spec['dsl'] && cond_spec['json']
35+
raise ArgumentError, "Malformed condition specification for probe: #{config}"
36+
end
37+
compiled = EL::Compiler.new.compile(cond_spec['json'])
38+
EL::Expression.new(cond_spec['dsl'], compiled)
39+
end
3340
Probe.new(
3441
id: config.fetch("id"),
3542
type: type_symbol,
@@ -47,7 +54,7 @@ def build_from_remote_config(config)
4754
max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
4855
max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
4956
rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
50-
condition: (cond = config.dig('when', 'json')) && EL::Compiler.new.compile(cond), # steep:ignore
57+
condition: cond,
5158
)
5259
rescue KeyError => exc
5360
raise ArgumentError, "Malformed remote configuration entry for probe: #{exc.class}: #{exc}: #{config}"
@@ -59,7 +66,11 @@ def build_template_segments(segments)
5966
if str = segment['str']
6067
str
6168
elsif ast = segment['json']
62-
EL::Compiler.new.compile(ast)
69+
unless dsl = segment['dsl']
70+
raise ArgumentError, "Missing dsl for json in segment: #{segment}"
71+
end
72+
compiled = EL::Compiler.new.compile(ast)
73+
EL::Expression.new(dsl, compiled)
6374
else
6475
# TODO report to telemetry?
6576
end

lib/datadog/di/probe_notification_builder.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ def evaluate_template(template_segments, context)
188188
raise ArgumentError, "Invalid template segment type: #{segment}"
189189
end
190190
rescue => exc
191-
evaluation_errors << "#{exc.class}: #{exc}"
191+
evaluation_errors << {
192+
message: "#{exc.class}: #{exc}",
193+
expr: segment.dsl_expr,
194+
}
192195
'[evaluation error]'
193196
end.join
194197
[message, evaluation_errors]

sig/datadog/di/el/expression.rbs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ module Datadog
22
module DI
33
module EL
44
class Expression
5-
@compiled_expr: untyped
5+
@dsl_expr: untyped
66

7-
def initialize: (untyped compiled_expr) -> void
7+
@evaluator: untyped
88

9-
attr_reader compiled_expr: untyped
9+
def initialize: (untyped dsl_expr, untyped compiled_expr) -> void
1010

11-
def ==: (untyped other) -> untyped
11+
attr_reader dsl_expr: untyped
12+
13+
attr_reader evaluator: untyped
1214

1315
def evaluate: (untyped context) -> untyped
1416

sig/datadog/di/probe.rbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Datadog
2525
@executed_on_line: bool?
2626

2727
def initialize: (id: String, type: Symbol, ?file: String?, ?line_no: Integer?, ?type_name: String?, ?method_name: String?, ?template: String?, ?template_segments: Array[untyped]?, ?capture_snapshot: bool,
28+
?condition: DI::EL::Expression?,
2829
?max_capture_depth: Integer, ?max_capture_attribute_count: Integer?, ?rate_limit: Integer) -> void
2930

3031
attr_reader condition: DI::EL::Expression?

spec/datadog/di/el/integration_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ class ELTestIvarClass
4040
let(:expected) { spec.fetch('compiled') }
4141

4242
let(:compiled) { compiler.compile(ast) }
43+
let(:expr) { Datadog::DI::EL::Expression.new('(expression)', compiled) }
4344

4445
let(:evaluated) do
45-
compiled.evaluate(context)
46+
expr.evaluate(context)
4647
end
4748

4849
let(:context) do

spec/datadog/di/instrumenter_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@
933933
context 'when condition is met' do
934934
let(:condition) do
935935
Datadog::DI::EL::Expression.new(
936+
'(expression)',
936937
# We use "arg1" here, actual variable name is not currently available
937938
"ref('arg1') == 41"
938939
)
@@ -944,6 +945,7 @@
944945
context 'when condition is not met' do
945946
let(:condition) do
946947
Datadog::DI::EL::Expression.new(
948+
'(expression)',
947949
# We use "arg1" here, actual variable name is not currently available
948950
"ref('arg1') == 42"
949951
)
@@ -957,6 +959,7 @@
957959
context 'when condition is met' do
958960
let(:condition) do
959961
Datadog::DI::EL::Expression.new(
962+
'(expression)',
960963
"ref('kwarg') == 42"
961964
)
962965
end
@@ -967,6 +970,7 @@
967970
context 'when condition is not met' do
968971
let(:condition) do
969972
Datadog::DI::EL::Expression.new(
973+
'(expression)',
970974
"ref('kwarg') == 41"
971975
)
972976
end
@@ -980,6 +984,7 @@
980984

981985
let(:condition) do
982986
Datadog::DI::EL::Expression.new(
987+
'(expression)',
983988
"unknown_function('kwarg') == 42"
984989
)
985990
end
@@ -1451,6 +1456,7 @@
14511456
context 'when condition is met' do
14521457
let(:condition) do
14531458
Datadog::DI::EL::Expression.new(
1459+
'(expression)',
14541460
"ref('local') == 42"
14551461
)
14561462
end
@@ -1466,6 +1472,7 @@
14661472
context 'when condition is not met' do
14671473
let(:condition) do
14681474
Datadog::DI::EL::Expression.new(
1475+
'(expression)',
14691476
"ref('local') == 43"
14701477
)
14711478
end
@@ -1489,6 +1496,7 @@
14891496
context 'when condition is met' do
14901497
let(:condition) do
14911498
Datadog::DI::EL::Expression.new(
1499+
'(expression)',
14921500
"iref('@ivar') == 42"
14931501
)
14941502
end
@@ -1504,6 +1512,7 @@
15041512
context 'when condition is not met' do
15051513
let(:condition) do
15061514
Datadog::DI::EL::Expression.new(
1515+
'(expression)',
15071516
"iref('@ivar') == 43"
15081517
)
15091518
end

spec/datadog/di/integration/everything_from_remote_config_spec.rb

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,25 +398,125 @@ def assert_received_and_errored
398398
where: {
399399
typeName: 'EverythingFromRemoteConfigSpecTestClass', methodName: 'target_method',
400400
},
401-
when: {json: {foo: 'bar'}},
401+
when: {json: {foo: 'bar'}, dsl: '(expression)'},
402402
}
403403
end
404404

405405
let(:propagate_all_exceptions) { false }
406406

407-
it 'catches the exception' do
407+
it 'catches the exception and reports probe status error' do
408408
expect_lazy_log(logger, :debug, /di: unhandled exception handling a probe in DI remote receiver: Datadog::DI::Error::InvalidExpression: Unknown operation: foo/)
409409

410410
do_rc(expect_add_probe: false)
411411
expect(probe_manager.installed_probes.length).to eq 0
412412

413413
payload = payloads.first
414414
expect(payload).to be_a(Hash)
415+
expect(payload).to match(
416+
ddsource: 'dd_debugger',
417+
debugger: {
418+
diagnostics: {
419+
parentId: nil,
420+
probeId: '11',
421+
probeVersion: 0,
422+
runtimeId: String,
423+
status: 'ERROR',
424+
},
425+
},
426+
path: '/debugger/v1/diagnostics',
427+
service: 'rspec',
428+
timestamp: Integer,
429+
message: String,
430+
)
415431
expect(payload[:message]).to match(
416432
/Instrumentation for probe .* failed: Unknown operation: foo/,
417433
)
418434
end
419435
end
436+
437+
context 'when there is a message template' do
438+
let(:probe_spec) do
439+
{
440+
id: '11', name: 'bar', type: 'LOG_PROBE',
441+
where: {
442+
typeName: 'EverythingFromRemoteConfigSpecTestClass', methodName: 'target_method',
443+
},
444+
segments: [
445+
# String segment
446+
{str: 'hello '},
447+
# Expression segment - valid at runtime
448+
{json: {eq: [{ref: '@ivar'}, 51]}, dsl: '(good expression)'},
449+
# Another expression which fails evaluation at runtime
450+
{json: {filter: [{ref: '@ivar'}, 'x']}, dsl: '(failing expression)'},
451+
],
452+
}
453+
end
454+
455+
let(:expected_snapshot_payload) do
456+
{
457+
path: '/debugger/v1/input',
458+
# We do not have active span/trace in the test.
459+
"dd.span_id": nil,
460+
"dd.trace_id": nil,
461+
"debugger.snapshot": {
462+
captures: nil,
463+
evaluationErrors: [
464+
{'expr' => '(failing expression)', 'message' => 'Datadog::DI::Error::ExpressionEvaluationError: Bad collection type for filter: NilClass'},
465+
],
466+
id: LOWERCASE_UUID_REGEXP,
467+
language: 'ruby',
468+
probe: {
469+
id: '11',
470+
location: {
471+
method: 'target_method',
472+
type: 'EverythingFromRemoteConfigSpecTestClass',
473+
},
474+
version: 0,
475+
},
476+
stack: Array,
477+
timestamp: Integer,
478+
},
479+
ddsource: 'dd_debugger',
480+
duration: Integer,
481+
host: nil,
482+
logger: {
483+
method: 'target_method',
484+
name: nil,
485+
thread_id: nil,
486+
thread_name: 'Thread.main',
487+
version: 2,
488+
},
489+
# false is the result of first expression evaluation
490+
# second expression fails evaluation
491+
message: 'hello false[evaluation error]',
492+
service: 'rspec',
493+
timestamp: Integer,
494+
}
495+
end
496+
497+
it 'evaluates expressions and reports errors' do
498+
expect_lazy_log(logger, :debug, /di: received log probe/)
499+
500+
do_rc
501+
assert_received_and_installed
502+
503+
# invocation
504+
505+
expect(EverythingFromRemoteConfigSpecTestClass.new.target_method).to eq 42
506+
507+
component.probe_notifier_worker.flush
508+
509+
# assertions
510+
511+
expect(payloads.length).to eq 2
512+
513+
emitting_payload = payloads.shift
514+
expect(emitting_payload).to match(expected_emitting_payload)
515+
516+
snapshot_payload = payloads.shift
517+
expect(order_hash_keys(snapshot_payload)).to match(deep_stringify_keys(order_hash_keys(expected_snapshot_payload)))
518+
end
519+
end
420520
end
421521

422522
context 'line probe' do
@@ -514,7 +614,7 @@ def assert_received_and_errored
514614
where: {
515615
sourceFile: 'hook_line_load.rb', lines: [14],
516616
},
517-
when: {json: {'contains' => [{'ref' => 'bar'}, 'baz']}},
617+
when: {json: {'contains' => [{'ref' => 'bar'}, 'baz']}, dsl: '(expression)'},
518618
}
519619
end
520620

spec/datadog/di/integration/instrumentation_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def run_test
509509
let(:segments) do
510510
[
511511
{str: 'hello '},
512-
{json: {ref: '@duration'}},
512+
{json: {ref: '@duration'}, dsl: '@duration'},
513513
{str: ' ms'},
514514
]
515515
end
@@ -538,7 +538,7 @@ def run_test
538538
let(:segments) do
539539
[
540540
{str: 'hello '},
541-
{json: {ref: '@return'}},
541+
{json: {ref: '@return'}, dsl: '@return'},
542542
]
543543
end
544544

@@ -560,7 +560,7 @@ def run_test
560560
let(:segments) do
561561
[
562562
{str: 'hello '},
563-
{json: {ref: '@exception'}},
563+
{json: {ref: '@exception'}, dsl: '@exception'},
564564
]
565565
end
566566

0 commit comments

Comments
 (0)