datadog 2.21.0 → 2.22.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +60 -0
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/libdatadog_api/ddsketch.c +106 -0
  6. data/ext/libdatadog_api/init.c +3 -0
  7. data/ext/libdatadog_api/library_config.c +35 -27
  8. data/ext/libdatadog_api/process_discovery.c +19 -13
  9. data/ext/libdatadog_extconf_helpers.rb +1 -1
  10. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +26 -0
  11. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +59 -0
  12. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +29 -0
  13. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +26 -0
  14. data/lib/datadog/appsec/api_security/endpoint_collection.rb +10 -0
  15. data/lib/datadog/appsec/assets/waf_rules/README.md +30 -36
  16. data/lib/datadog/appsec/assets/waf_rules/recommended.json +359 -4
  17. data/lib/datadog/appsec/assets/waf_rules/strict.json +43 -2
  18. data/lib/datadog/appsec/compressed_json.rb +1 -1
  19. data/lib/datadog/appsec/configuration/settings.rb +9 -0
  20. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +3 -1
  21. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +3 -2
  22. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +3 -1
  23. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +3 -1
  24. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -4
  25. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +5 -1
  26. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +7 -2
  27. data/lib/datadog/appsec/contrib/rails/patcher.rb +30 -0
  28. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +3 -1
  29. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +10 -4
  30. data/lib/datadog/appsec/event.rb +12 -14
  31. data/lib/datadog/appsec/metrics/collector.rb +19 -3
  32. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +2 -1
  33. data/lib/datadog/appsec/monitor/gateway/watcher.rb +4 -4
  34. data/lib/datadog/appsec/remote.rb +25 -13
  35. data/lib/datadog/appsec/security_engine/result.rb +28 -9
  36. data/lib/datadog/appsec/security_engine/runner.rb +17 -7
  37. data/lib/datadog/appsec/security_event.rb +5 -7
  38. data/lib/datadog/core/configuration/components.rb +14 -6
  39. data/lib/datadog/core/configuration/stable_config.rb +10 -0
  40. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  41. data/lib/datadog/core/configuration.rb +1 -1
  42. data/lib/datadog/core/ddsketch.rb +21 -0
  43. data/lib/datadog/core/environment/yjit.rb +2 -1
  44. data/lib/datadog/core/pin.rb +4 -8
  45. data/lib/datadog/core/process_discovery.rb +4 -2
  46. data/lib/datadog/core/remote/component.rb +4 -6
  47. data/lib/datadog/core/telemetry/component.rb +11 -0
  48. data/lib/datadog/core/telemetry/emitter.rb +6 -6
  49. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +30 -0
  50. data/lib/datadog/core/telemetry/event.rb +1 -0
  51. data/lib/datadog/core/transport/response.rb +4 -1
  52. data/lib/datadog/core/utils/network.rb +19 -0
  53. data/lib/datadog/di/boot.rb +1 -0
  54. data/lib/datadog/di/component.rb +14 -0
  55. data/lib/datadog/di/context.rb +70 -0
  56. data/lib/datadog/di/el/compiler.rb +164 -0
  57. data/lib/datadog/di/el/evaluator.rb +159 -0
  58. data/lib/datadog/di/el/expression.rb +42 -0
  59. data/lib/datadog/di/el.rb +5 -0
  60. data/lib/datadog/di/error.rb +25 -0
  61. data/lib/datadog/di/instrumenter.rb +101 -32
  62. data/lib/datadog/di/probe.rb +35 -15
  63. data/lib/datadog/di/probe_builder.rb +39 -1
  64. data/lib/datadog/di/probe_manager.rb +3 -2
  65. data/lib/datadog/di/probe_notification_builder.rb +50 -51
  66. data/lib/datadog/di/serializer.rb +151 -7
  67. data/lib/datadog/tracing/component.rb +6 -17
  68. data/lib/datadog/tracing/configuration/dynamic.rb +2 -2
  69. data/lib/datadog/tracing/configuration/settings.rb +3 -3
  70. data/lib/datadog/tracing/contrib/component.rb +2 -2
  71. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +7 -0
  72. data/lib/datadog/tracing/contrib/graphql/ext.rb +1 -0
  73. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +53 -28
  74. data/lib/datadog/tracing/metadata/ext.rb +8 -0
  75. data/lib/datadog/version.rb +1 -1
  76. metadata +22 -9
  77. data/ext/libdatadog_api/macos_development.md +0 -26
@@ -10,6 +10,7 @@ module Datadog
10
10
  {"DD_AGENT_HOST" => {version: ["A"]},
11
11
  "DD_API_KEY" => {version: ["A"]},
12
12
  "DD_API_SECURITY_ENABLED" => {version: ["A"]},
13
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED" => {version: ["A"]},
13
14
  "DD_API_SECURITY_REQUEST_SAMPLE_RATE" => {version: ["A"]},
14
15
  "DD_API_SECURITY_SAMPLE_DELAY" => {version: ["A"]},
15
16
  "DD_APM_TRACING_ENABLED" => {version: ["A"]},
@@ -171,6 +172,7 @@ module Datadog
171
172
  "DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE" => {version: ["A"]},
172
173
  "DD_TRACE_GRAPHQL_ENABLED" => {version: ["A"]},
173
174
  "DD_TRACE_GRAPHQL_ERROR_EXTENSIONS" => {version: ["A"]},
175
+ "DD_TRACE_GRAPHQL_ERROR_TRACKING" => {version: ["A"]},
174
176
  "DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER" => {version: ["A"]},
175
177
  "DD_TRACE_GRPC_ANALYTICS_ENABLED" => {version: ["A"]},
176
178
  "DD_TRACE_GRPC_ANALYTICS_SAMPLE_RATE" => {version: ["A"]},
@@ -238,7 +238,7 @@ module Datadog
238
238
  yield write_components
239
239
  rescue ThreadError => e
240
240
  logger_without_components.error(
241
- 'Detected deadlock during datadog initialization. ' \
241
+ "Detected deadlock during datadog initialization: #{e.class}: #{e}. " \
242
242
  'Please report this at https://github.com/datadog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug' \
243
243
  "\n\tSource:\n\t#{Array(e.backtrace).join("\n\t")}"
244
244
  )
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'datadog/core'
4
+
5
+ module Datadog
6
+ module Core
7
+ # Used to access ddsketch APIs.
8
+ # APIs in this class are implemented as native code.
9
+ class DDSketch
10
+ def self.supported?
11
+ Datadog::Core::LIBDATADOG_API_FAILURE.nil?
12
+ end
13
+
14
+ def initialize
15
+ unless self.class.supported?
16
+ raise(ArgumentError, "DDSketch is not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -54,7 +54,8 @@ module Datadog
54
54
 
55
55
  # Ratio of YJIT-executed instructions
56
56
  def ratio_in_yjit
57
- ::RubyVM::YJIT.runtime_stats[:ratio_in_yjit]
57
+ stats = ::RubyVM::YJIT.runtime_stats
58
+ stats[:ratio_in_yjit] if stats.key?(:ratio_in_yjit)
58
59
  end
59
60
 
60
61
  def available?
@@ -43,18 +43,14 @@ module Datadog
43
43
  # rubocop:disable Style/TrivialAccessors
44
44
  def onto(obj)
45
45
  unless obj.respond_to? :datadog_pin=
46
- obj.instance_exec do
47
- def datadog_pin=(pin)
48
- @datadog_pin = pin
49
- end
46
+ obj.define_singleton_method(:datadog_pin=) do |pin|
47
+ @datadog_pin = pin
50
48
  end
51
49
  end
52
50
 
53
51
  unless obj.respond_to? :datadog_pin
54
- obj.instance_exec do
55
- def datadog_pin
56
- @datadog_pin
57
- end
52
+ obj.define_singleton_method(:datadog_pin) do
53
+ @datadog_pin
58
54
  end
59
55
  end
60
56
 
@@ -37,14 +37,16 @@ module Datadog
37
37
  # In the C method exposed by ddcommon, memfd_create replaces empty strings by None for these fields.
38
38
  def get_metadata(settings)
39
39
  {
40
- schema_version: 1,
41
40
  runtime_id: Core::Environment::Identity.id,
42
41
  tracer_language: Core::Environment::Identity.lang,
43
42
  tracer_version: Core::Environment::Identity.gem_datadog_version_semver2,
44
43
  hostname: Core::Environment::Socket.hostname,
45
44
  service_name: settings.service || '',
46
45
  service_env: settings.env || '',
47
- service_version: settings.version || ''
46
+ service_version: settings.version || '',
47
+ # TODO: Implement process tags and container id
48
+ process_tags: '',
49
+ container_id: ''
48
50
  }
49
51
  end
50
52
 
@@ -135,13 +135,11 @@ module Datadog
135
135
 
136
136
  # Release all current waiters
137
137
  def lift
138
- @mutex.lock
138
+ @mutex.synchronize do
139
+ @once ||= true
139
140
 
140
- @once ||= true
141
-
142
- @condition.broadcast
143
- ensure
144
- @mutex.unlock
141
+ @condition.broadcast
142
+ end
145
143
  end
146
144
  end
147
145
 
@@ -19,6 +19,8 @@ module Datadog
19
19
  #
20
20
  # @api private
21
21
  class Component
22
+ ENDPOINT_COLLECTION_MESSAGE_LIMIT = 300
23
+
22
24
  attr_reader :enabled, :logger, :transport, :worker
23
25
 
24
26
  include Core::Utils::Forking
@@ -165,6 +167,15 @@ module Datadog
165
167
  @worker.enqueue(Event::AppClientConfigurationChange.new(changes, 'remote_config'))
166
168
  end
167
169
 
170
+ # Report application endpoints
171
+ def app_endpoints_loaded(endpoints, page_size: ENDPOINT_COLLECTION_MESSAGE_LIMIT)
172
+ return if !@enabled || forked?
173
+
174
+ endpoints.each_slice(page_size).with_index do |endpoints_slice, i|
175
+ @worker.enqueue(Event::AppEndpointsLoaded.new(endpoints_slice, is_first: i.zero?))
176
+ end
177
+ end
178
+
168
179
  # Increments a count metric.
169
180
  def inc(namespace, metric_name, value, tags: {}, common: true)
170
181
  @metrics_manager.inc(namespace, metric_name, value, tags: tags, common: common)
@@ -31,15 +31,15 @@ module Datadog
31
31
  seq_id = self.class.sequence.next
32
32
  payload = Request.build_payload(event, seq_id, debug: debug?)
33
33
  res = @transport.send_telemetry(request_type: event.type, payload: payload)
34
- logger.debug { "Telemetry sent for event `#{event.type}` (response code: #{res.code})" }
34
+ if res.ok?
35
+ logger.debug { "Telemetry sent for event `#{event.type}`" }
36
+ else
37
+ logger.debug { "Failed to send telemetry for event `#{event.type}`: #{res.inspect}" }
38
+ end
35
39
  res
36
40
  rescue => e
37
41
  logger.debug {
38
- "Unable to send telemetry request for event `#{begin
39
- event.type
40
- rescue
41
- "unknown"
42
- end}`: #{e}"
42
+ "Unable to send telemetry request for event `#{event.respond_to?(:type) ? event.type : event.to_s}`: #{e.class}: #{e}"
43
43
  }
44
44
  Core::Transport::InternalErrorResponse.new(e)
45
45
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Datadog
6
+ module Core
7
+ module Telemetry
8
+ module Event
9
+ # Telemetry event class for sending 'app-endpoints' payload
10
+ class AppEndpointsLoaded < Base
11
+ def initialize(endpoints, is_first:)
12
+ @endpoints = endpoints
13
+ @is_first = !!is_first
14
+ end
15
+
16
+ def type
17
+ 'app-endpoints'
18
+ end
19
+
20
+ def payload
21
+ {
22
+ is_first: @is_first,
23
+ endpoints: @endpoints
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -26,6 +26,7 @@ require_relative 'event/base'
26
26
  require_relative 'event/app_client_configuration_change'
27
27
  require_relative 'event/app_closing'
28
28
  require_relative 'event/app_dependencies_loaded'
29
+ require_relative 'event/app_endpoints_loaded'
29
30
  require_relative 'event/app_heartbeat'
30
31
  require_relative 'event/app_integrations_change'
31
32
  require_relative 'event/app_started'
@@ -34,7 +34,10 @@ module Datadog
34
34
  end
35
35
 
36
36
  def inspect
37
- "#{self.class} ok?:#{ok?} unsupported?:#{unsupported?}, " \
37
+ maybe_code = if respond_to?(:code)
38
+ " code:#{code}," # steep:ignore
39
+ end
40
+ "#{self.class} ok?:#{ok?},#{maybe_code} unsupported?:#{unsupported?}, " \
38
41
  "not_found?:#{not_found?}, client_error?:#{client_error?}, " \
39
42
  "server_error?:#{server_error?}, internal_error?:#{internal_error?}, " \
40
43
  "payload:#{payload}"
@@ -13,6 +13,7 @@ module Datadog
13
13
  true-client-ip
14
14
  x-client-ip
15
15
  x-forwarded
16
+ forwarded
16
17
  forwarded-for
17
18
  x-cluster-client-ip
18
19
  fastly-client-ip
@@ -73,6 +74,8 @@ module Datadog
73
74
  next unless value
74
75
 
75
76
  ips = value.split(',')
77
+ ips = process_forwarded_header_values(ips) if name == 'forwarded'
78
+
76
79
  ips.each do |ip|
77
80
  parsed_ip = ip_to_ipaddr(ip.strip)
78
81
 
@@ -83,6 +86,22 @@ module Datadog
83
86
  nil
84
87
  end
85
88
 
89
+ def process_forwarded_header_values(values)
90
+ values.each_with_object([]) do |value, acc|
91
+ value.downcase!
92
+
93
+ value.split(';').each do |tuple_str|
94
+ tuple_str.strip!
95
+ next unless tuple_str.start_with?('for=')
96
+
97
+ tuple_str.delete_prefix!('for=')
98
+ tuple_str.delete!('"')
99
+
100
+ acc << tuple_str
101
+ end
102
+ end
103
+ end
104
+
86
105
  # Returns whether the given value is more likely to be an IPv4 than an IPv6 address.
87
106
  #
88
107
  # This is done by checking if a dot (`'.'`) character appears before a colon (`':'`) in the value.
@@ -5,6 +5,7 @@ require_relative 'base'
5
5
  require_relative 'error'
6
6
  require_relative 'code_tracker'
7
7
  require_relative 'component'
8
+ require_relative 'context'
8
9
  require_relative 'instrumenter'
9
10
  require_relative 'probe'
10
11
  require_relative 'probe_builder'
@@ -115,6 +115,20 @@ module Datadog
115
115
 
116
116
  def parse_probe_spec_and_notify(probe_spec)
117
117
  probe = ProbeBuilder.build_from_remote_config(probe_spec)
118
+ rescue => exc
119
+ begin
120
+ probe = Struct.new(:id).new(
121
+ probe_spec['id'],
122
+ )
123
+ payload = probe_notification_builder.build_errored(probe, exc)
124
+ probe_notifier_worker.add_status(payload)
125
+ rescue # standard:disable Lint/UselessRescue
126
+ # TODO report via instrumentation telemetry?
127
+ raise
128
+ end
129
+
130
+ raise
131
+ else
118
132
  payload = probe_notification_builder.build_received(probe)
119
133
  probe_notifier_worker.add_status(payload)
120
134
  probe
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Contains local and instance variables used when evaluating
6
+ # expressions in DI Expression Language.
7
+ #
8
+ # @api private
9
+ class Context
10
+ def initialize(probe:, settings:, serializer:, locals: nil,
11
+ # In Ruby everything is a method, therefore we should always have
12
+ # a target self. However, if we are not capturing a snapshot,
13
+ # there is no need to pass in the target self.
14
+ target_self: nil,
15
+ path: nil, caller_locations: nil,
16
+ serialized_entry_args: nil,
17
+ return_value: nil, duration: nil, exception: nil)
18
+ @probe = probe
19
+ @settings = settings
20
+ @serializer = serializer
21
+ @locals = locals
22
+ @target_self = target_self
23
+ @path = path
24
+ @caller_locations = caller_locations
25
+ @serialized_entry_args = serialized_entry_args
26
+ @return_value = return_value
27
+ @duration = duration
28
+ @exception = exception
29
+ end
30
+
31
+ attr_reader :probe
32
+ attr_reader :settings
33
+ attr_reader :serializer
34
+ attr_reader :locals
35
+ attr_reader :target_self
36
+ # Actual path of the instrumented file.
37
+ attr_reader :path
38
+ # TODO check how many stack frames we should be keeping/sending,
39
+ # this should be all frames for enriched probes and no frames for
40
+ # non-enriched probes?
41
+ attr_reader :caller_locations
42
+ attr_reader :serialized_entry_args
43
+ # Return value for the method, for a method probe
44
+ attr_reader :return_value
45
+ # How long the method took to execute, for a method probe
46
+ attr_reader :duration
47
+ # Exception raised by the method, if any, for a method probe
48
+ attr_reader :exception
49
+
50
+ def serialized_locals
51
+ # TODO cache?
52
+ locals && serializer.serialize_vars(locals,
53
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
54
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)
55
+ end
56
+
57
+ def fetch(var_name)
58
+ unless locals
59
+ # TODO return "undefined" instead?
60
+ return nil
61
+ end
62
+ locals[var_name.to_sym]
63
+ end
64
+
65
+ def fetch_ivar(var_name)
66
+ target_self.instance_variable_get(var_name)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # DI Expression Language compiler.
7
+ #
8
+ # Converts AST in probe definitions into Expression objects.
9
+ #
10
+ # WARNING: this class produces strings that are then eval'd as
11
+ # Ruby code. Input ASTs are user-controlled. As such the compiler
12
+ # must sanitize and escape all input to avoid injection.
13
+ #
14
+ # Besides quotes and backslashes we must also escape # which is
15
+ # starting string interpolation (#{...}).
16
+ #
17
+ # @api private
18
+ class Compiler
19
+ def compile(ast)
20
+ compile_partial(ast)
21
+ end
22
+
23
+ private
24
+
25
+ OPERATORS = {
26
+ 'eq' => '==',
27
+ 'ne' => '!=',
28
+ 'ge' => '>=',
29
+ 'gt' => '>',
30
+ 'le' => '<=',
31
+ 'lt' => '<',
32
+ }.freeze
33
+
34
+ SINGLE_ARG_METHODS = %w[
35
+ len isEmpty isUndefined
36
+ ].freeze
37
+
38
+ TWO_ARG_METHODS = %w[
39
+ startsWith endsWith contains matches
40
+ getmember index instanceof
41
+ ].freeze
42
+
43
+ MULTI_ARG_METHODS = {
44
+ 'and' => '&&',
45
+ 'or' => '||',
46
+ }.freeze
47
+
48
+ def compile_partial(ast)
49
+ case ast
50
+ when Hash
51
+ if ast.length != 1
52
+ raise DI::Error::InvalidExpression, "Expected hash of length 1: #{ast}"
53
+ end
54
+ op, target = ast.first
55
+ case op
56
+ when 'ref'
57
+ unless String === target
58
+ raise DI::Error::InvalidExpression, "Bad ref value type: #{target.class}: #{target}"
59
+ end
60
+ case target
61
+ when '@it'
62
+ 'current_item'
63
+ when '@key'
64
+ 'current_key'
65
+ when '@value'
66
+ 'current_value'
67
+ when '@return'
68
+ # For @return, @duration and @exception we shadow
69
+ # instance variables.
70
+ "context.return_value"
71
+ when '@duration'
72
+ # There is no way to explicitly format the duration.
73
+ # TODO come up with better formatting?
74
+ # We could format to a string here but what if customer
75
+ # has @duration as part of an expression and wants
76
+ # to retain it as a number?
77
+ "(context.duration * 1000)"
78
+ when '@exception'
79
+ "context.exception"
80
+ else
81
+ # Ruby technically allows all kinds of symbols in variable
82
+ # names, for example spaces and many characters.
83
+ # Start out with strict validation to avoid possible
84
+ # surprises and need to escape.
85
+ unless target =~ %r{\A(@?)([a-zA-Z0-9_]+)\z}
86
+ raise DI::Error::BadVariableName, "Bad variable name: #{target}"
87
+ end
88
+ method_name = (($1 == '@') ? 'iref' : 'ref')
89
+ "#{method_name}('#{target}')"
90
+ end
91
+ when *SINGLE_ARG_METHODS
92
+ method_name = op.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }
93
+ "#{method_name}(#{compile_partial(target)}, '#{var_name_maybe(target)}')"
94
+ when *TWO_ARG_METHODS
95
+ method_name = op.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }
96
+ unless Array === target && target.length == 2
97
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
98
+ end
99
+ first, second = target
100
+ "#{method_name}(#{compile_partial(first)}, (#{compile_partial(second)}))"
101
+ when *MULTI_ARG_METHODS.keys
102
+ unless Array === target && target.length >= 1
103
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
104
+ end
105
+ compiled_targets = target.map do |item|
106
+ "(#{compile_partial(item)})"
107
+ end
108
+ compiled_op = MULTI_ARG_METHODS[op]
109
+ "(#{compiled_targets.join(" #{compiled_op} ")})"
110
+ when 'substring'
111
+ unless Array === target && target.length == 3
112
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
113
+ end
114
+ "#{op}(#{target.map { |arg| "(#{compile_partial(arg)})" }.join(", ")})"
115
+ when 'not'
116
+ "!(#{compile_partial(target)})"
117
+ when *OPERATORS.keys
118
+ unless Array === target && target.length == 2
119
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
120
+ end
121
+ first, second = target
122
+ operator = OPERATORS.fetch(op)
123
+ "(#{compile_partial(first)}) #{operator} (#{compile_partial(second)})"
124
+ when 'any', 'all', 'filter'
125
+ "#{op}(#{compile_partial(target.first)}) { |current_item, current_key, current_value| #{compile_partial(target.last)} }"
126
+ else
127
+ raise DI::Error::InvalidExpression, "Unknown operation: #{op}"
128
+ end
129
+ when Numeric, true, false, nil
130
+ # No escaping is needed for the values here.
131
+ ast.inspect
132
+ when String
133
+ "\"#{escape(ast)}\""
134
+ when Array
135
+ # Arrays are commonly used as arguments of operators/methods,
136
+ # but there are no arrays at the top level in the syntax that
137
+ # we currently understand. Provide a helpful error message in case
138
+ # syntax is expanded in the future.
139
+ raise DI::Error::InvalidExpression, "Array is not valid at its location, do you need to upgrade dd-trace-rb? #{ast}"
140
+ else
141
+ raise DI::Error::InvalidExpression, "Unknown type in AST: #{ast}"
142
+ end
143
+ end
144
+
145
+ # Returns a textual description of +target+ for use in exception
146
+ # messages. +target+ could be any expression language expression.
147
+ # WARNING: the result of this method is included in eval'd code,
148
+ # it must be sanitized to avoid injection.
149
+ def var_name_maybe(target)
150
+ if Hash === target && target.length == 1 && target.keys.first == 'ref' &&
151
+ String === (value = target.values.first)
152
+ escape(value)
153
+ else
154
+ '(expression)'
155
+ end
156
+ end
157
+
158
+ def escape(needle)
159
+ needle.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub('#') { "\\#" }
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end