datadog 2.21.0 → 2.23.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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -2
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +3 -0
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +4 -0
  6. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +1 -1
  7. data/ext/datadog_profiling_native_extension/extconf.rb +6 -4
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +1 -1
  9. data/ext/libdatadog_api/datadog_ruby_common.h +1 -1
  10. data/ext/libdatadog_api/ddsketch.c +106 -0
  11. data/ext/libdatadog_api/feature_flags.c +554 -0
  12. data/ext/libdatadog_api/feature_flags.h +5 -0
  13. data/ext/libdatadog_api/init.c +5 -0
  14. data/ext/libdatadog_api/library_config.c +34 -25
  15. data/ext/libdatadog_api/process_discovery.c +19 -13
  16. data/ext/libdatadog_extconf_helpers.rb +1 -1
  17. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +26 -0
  18. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +59 -0
  19. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +29 -0
  20. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +26 -0
  21. data/lib/datadog/appsec/api_security/endpoint_collection.rb +10 -0
  22. data/lib/datadog/appsec/api_security/route_extractor.rb +23 -6
  23. data/lib/datadog/appsec/api_security/sampler.rb +7 -4
  24. data/lib/datadog/appsec/assets/blocked.html +8 -0
  25. data/lib/datadog/appsec/assets/blocked.json +1 -1
  26. data/lib/datadog/appsec/assets/blocked.text +3 -1
  27. data/lib/datadog/appsec/assets/waf_rules/README.md +30 -36
  28. data/lib/datadog/appsec/assets/waf_rules/recommended.json +359 -4
  29. data/lib/datadog/appsec/assets/waf_rules/strict.json +43 -2
  30. data/lib/datadog/appsec/assets.rb +1 -1
  31. data/lib/datadog/appsec/compressed_json.rb +1 -1
  32. data/lib/datadog/appsec/configuration/settings.rb +9 -0
  33. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +3 -1
  34. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +3 -2
  35. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +3 -1
  36. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +3 -1
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -4
  38. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +5 -1
  39. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +7 -2
  40. data/lib/datadog/appsec/contrib/rails/patcher.rb +30 -0
  41. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +3 -1
  42. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +10 -4
  43. data/lib/datadog/appsec/event.rb +12 -14
  44. data/lib/datadog/appsec/metrics/collector.rb +19 -3
  45. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +2 -1
  46. data/lib/datadog/appsec/monitor/gateway/watcher.rb +4 -4
  47. data/lib/datadog/appsec/remote.rb +29 -13
  48. data/lib/datadog/appsec/response.rb +18 -4
  49. data/lib/datadog/appsec/security_engine/result.rb +28 -9
  50. data/lib/datadog/appsec/security_engine/runner.rb +17 -7
  51. data/lib/datadog/appsec/security_event.rb +5 -7
  52. data/lib/datadog/core/configuration/components.rb +44 -9
  53. data/lib/datadog/core/configuration/config_helper.rb +1 -1
  54. data/lib/datadog/core/configuration/settings.rb +14 -0
  55. data/lib/datadog/core/configuration/stable_config.rb +10 -0
  56. data/lib/datadog/core/configuration/supported_configurations.rb +330 -299
  57. data/lib/datadog/core/configuration.rb +1 -1
  58. data/lib/datadog/core/ddsketch.rb +19 -0
  59. data/lib/datadog/core/environment/ext.rb +6 -0
  60. data/lib/datadog/core/environment/process.rb +79 -0
  61. data/lib/datadog/core/environment/yjit.rb +2 -1
  62. data/lib/datadog/core/feature_flags.rb +61 -0
  63. data/lib/datadog/core/pin.rb +4 -8
  64. data/lib/datadog/core/process_discovery.rb +4 -2
  65. data/lib/datadog/core/remote/client/capabilities.rb +7 -0
  66. data/lib/datadog/core/remote/component.rb +4 -6
  67. data/lib/datadog/core/remote/transport/config.rb +2 -10
  68. data/lib/datadog/core/remote/transport/http/config.rb +9 -9
  69. data/lib/datadog/core/remote/transport/http/negotiation.rb +17 -8
  70. data/lib/datadog/core/remote/transport/http.rb +2 -0
  71. data/lib/datadog/core/remote/transport/negotiation.rb +2 -18
  72. data/lib/datadog/core/remote/worker.rb +25 -37
  73. data/lib/datadog/core/tag_builder.rb +0 -4
  74. data/lib/datadog/core/tag_normalizer.rb +84 -0
  75. data/lib/datadog/core/telemetry/component.rb +18 -3
  76. data/lib/datadog/core/telemetry/emitter.rb +6 -6
  77. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +30 -0
  78. data/lib/datadog/core/telemetry/event/app_started.rb +52 -49
  79. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
  80. data/lib/datadog/core/telemetry/event.rb +1 -0
  81. data/lib/datadog/core/telemetry/logger.rb +2 -2
  82. data/lib/datadog/core/telemetry/logging.rb +2 -8
  83. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +5 -6
  84. data/lib/datadog/core/telemetry/transport/telemetry.rb +1 -2
  85. data/lib/datadog/core/transport/http/client.rb +69 -0
  86. data/lib/datadog/core/transport/response.rb +4 -1
  87. data/lib/datadog/core/utils/array.rb +29 -0
  88. data/lib/datadog/{appsec/api_security → core/utils}/lru_cache.rb +10 -21
  89. data/lib/datadog/core/utils/network.rb +22 -1
  90. data/lib/datadog/core/utils/only_once_successful.rb +6 -2
  91. data/lib/datadog/core/utils.rb +2 -0
  92. data/lib/datadog/data_streams/configuration/settings.rb +49 -0
  93. data/lib/datadog/data_streams/configuration.rb +11 -0
  94. data/lib/datadog/data_streams/ext.rb +11 -0
  95. data/lib/datadog/data_streams/extensions.rb +16 -0
  96. data/lib/datadog/data_streams/pathway_context.rb +169 -0
  97. data/lib/datadog/data_streams/processor.rb +509 -0
  98. data/lib/datadog/data_streams/transport/http/api.rb +33 -0
  99. data/lib/datadog/data_streams/transport/http/client.rb +21 -0
  100. data/lib/datadog/data_streams/transport/http/stats.rb +87 -0
  101. data/lib/datadog/data_streams/transport/http.rb +41 -0
  102. data/lib/datadog/data_streams/transport/stats.rb +60 -0
  103. data/lib/datadog/data_streams.rb +100 -0
  104. data/lib/datadog/di/boot.rb +1 -0
  105. data/lib/datadog/di/component.rb +14 -16
  106. data/lib/datadog/di/context.rb +70 -0
  107. data/lib/datadog/di/el/compiler.rb +164 -0
  108. data/lib/datadog/di/el/evaluator.rb +159 -0
  109. data/lib/datadog/di/el/expression.rb +42 -0
  110. data/lib/datadog/di/el.rb +5 -0
  111. data/lib/datadog/di/error.rb +29 -0
  112. data/lib/datadog/di/instrumenter.rb +163 -48
  113. data/lib/datadog/di/probe.rb +55 -15
  114. data/lib/datadog/di/probe_builder.rb +39 -1
  115. data/lib/datadog/di/probe_manager.rb +13 -4
  116. data/lib/datadog/di/probe_notification_builder.rb +105 -67
  117. data/lib/datadog/di/proc_responder.rb +32 -0
  118. data/lib/datadog/di/serializer.rb +151 -7
  119. data/lib/datadog/di/transport/diagnostics.rb +2 -2
  120. data/lib/datadog/di/transport/http/diagnostics.rb +2 -4
  121. data/lib/datadog/di/transport/http/input.rb +2 -4
  122. data/lib/datadog/di/transport/http.rb +6 -2
  123. data/lib/datadog/di/transport/input.rb +64 -4
  124. data/lib/datadog/open_feature/component.rb +60 -0
  125. data/lib/datadog/open_feature/configuration.rb +27 -0
  126. data/lib/datadog/open_feature/evaluation_engine.rb +69 -0
  127. data/lib/datadog/open_feature/exposures/batch_builder.rb +32 -0
  128. data/lib/datadog/open_feature/exposures/buffer.rb +43 -0
  129. data/lib/datadog/open_feature/exposures/deduplicator.rb +30 -0
  130. data/lib/datadog/open_feature/exposures/event.rb +60 -0
  131. data/lib/datadog/open_feature/exposures/reporter.rb +40 -0
  132. data/lib/datadog/open_feature/exposures/worker.rb +116 -0
  133. data/lib/datadog/open_feature/ext.rb +14 -0
  134. data/lib/datadog/open_feature/native_evaluator.rb +38 -0
  135. data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
  136. data/lib/datadog/open_feature/provider.rb +141 -0
  137. data/lib/datadog/open_feature/remote.rb +74 -0
  138. data/lib/datadog/open_feature/resolution_details.rb +35 -0
  139. data/lib/datadog/open_feature/transport.rb +72 -0
  140. data/lib/datadog/open_feature.rb +19 -0
  141. data/lib/datadog/opentelemetry/configuration/settings.rb +159 -0
  142. data/lib/datadog/opentelemetry/metrics.rb +110 -0
  143. data/lib/datadog/opentelemetry/sdk/configurator.rb +25 -1
  144. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +38 -0
  145. data/lib/datadog/opentelemetry.rb +3 -0
  146. data/lib/datadog/profiling/collectors/code_provenance.rb +15 -6
  147. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  148. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -1
  149. data/lib/datadog/profiling/profiler.rb +4 -0
  150. data/lib/datadog/profiling/tag_builder.rb +36 -3
  151. data/lib/datadog/profiling.rb +1 -2
  152. data/lib/datadog/single_step_instrument.rb +1 -1
  153. data/lib/datadog/tracing/component.rb +6 -17
  154. data/lib/datadog/tracing/configuration/dynamic.rb +2 -2
  155. data/lib/datadog/tracing/configuration/ext.rb +9 -0
  156. data/lib/datadog/tracing/configuration/settings.rb +77 -3
  157. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -4
  158. data/lib/datadog/tracing/contrib/action_pack/utils.rb +1 -2
  159. data/lib/datadog/tracing/contrib/active_job/log_injection.rb +21 -7
  160. data/lib/datadog/tracing/contrib/active_job/patcher.rb +5 -1
  161. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +4 -2
  162. data/lib/datadog/tracing/contrib/component.rb +2 -2
  163. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -1
  164. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +11 -3
  165. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +11 -7
  166. data/lib/datadog/tracing/contrib/grape/configuration/settings.rb +7 -3
  167. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +7 -0
  168. data/lib/datadog/tracing/contrib/graphql/ext.rb +1 -0
  169. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +74 -44
  170. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +11 -3
  171. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +11 -3
  172. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +11 -3
  173. data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +66 -0
  174. data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +66 -0
  175. data/lib/datadog/tracing/contrib/kafka/patcher.rb +14 -0
  176. data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
  177. data/lib/datadog/tracing/contrib/karafka/monitor.rb +11 -0
  178. data/lib/datadog/tracing/contrib/karafka/patcher.rb +32 -0
  179. data/lib/datadog/tracing/contrib/rack/middlewares.rb +59 -27
  180. data/lib/datadog/tracing/contrib/rack/route_inference.rb +53 -0
  181. data/lib/datadog/tracing/contrib/rails/middlewares.rb +2 -2
  182. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +4 -1
  183. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +3 -1
  184. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +3 -1
  185. data/lib/datadog/tracing/contrib/status_range_matcher.rb +7 -0
  186. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +27 -0
  187. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +48 -0
  188. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +17 -0
  189. data/lib/datadog/tracing/contrib/waterdrop/integration.rb +43 -0
  190. data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +46 -0
  191. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +46 -0
  192. data/lib/datadog/tracing/contrib/waterdrop/producer.rb +50 -0
  193. data/lib/datadog/tracing/contrib/waterdrop.rb +37 -0
  194. data/lib/datadog/tracing/contrib.rb +1 -0
  195. data/lib/datadog/tracing/metadata/ext.rb +9 -1
  196. data/lib/datadog/tracing/transport/http/client.rb +12 -26
  197. data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
  198. data/lib/datadog/tracing/transport/traces.rb +3 -5
  199. data/lib/datadog/version.rb +2 -2
  200. data/lib/datadog.rb +2 -0
  201. metadata +92 -16
  202. data/ext/libdatadog_api/macos_development.md +0 -26
  203. data/lib/datadog/core/remote/transport/http/client.rb +0 -49
  204. data/lib/datadog/core/telemetry/transport/http/client.rb +0 -49
  205. data/lib/datadog/di/transport/http/client.rb +0 -47
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'msgpack'
4
+ require 'zlib'
5
+ require_relative '../../core/transport/parcel'
6
+ require_relative '../../core/transport/request'
7
+
8
+ module Datadog
9
+ module DataStreams
10
+ module Transport
11
+ module Stats
12
+ # Parcel for encoded DSM stats payload
13
+ class EncodedParcel
14
+ include Datadog::Core::Transport::Parcel
15
+
16
+ def initialize(data)
17
+ @data = data
18
+ end
19
+
20
+ attr_reader :data
21
+ end
22
+
23
+ # Request for DSM stats
24
+ class Request < Datadog::Core::Transport::Request
25
+ end
26
+
27
+ # Transport for Data Streams Monitoring stats
28
+ class Transport
29
+ attr_reader :client, :apis, :current_api_id, :logger
30
+
31
+ def initialize(apis, default_api, logger:)
32
+ @apis = apis
33
+ @logger = logger
34
+ @default_api = default_api
35
+ @current_api_id = default_api
36
+
37
+ @client = DataStreams::Transport::HTTP::Client.new(current_api, logger: @logger)
38
+ end
39
+
40
+ def send_stats(payload)
41
+ # MessagePack encode and gzip compress the payload
42
+ msgpack_data = MessagePack.pack(payload)
43
+ compressed_data = Zlib.gzip(msgpack_data)
44
+
45
+ # Create parcel and request
46
+ parcel = EncodedParcel.new(compressed_data)
47
+ request = Request.new(parcel)
48
+
49
+ # Send to agent
50
+ client.send_stats_payload(request)
51
+ end
52
+
53
+ def current_api
54
+ apis[@current_api_id]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_streams/processor'
4
+ require_relative 'data_streams/pathway_context'
5
+ require_relative 'data_streams/configuration/settings'
6
+ require_relative 'data_streams/extensions'
7
+ require_relative 'core/utils/time'
8
+
9
+ module Datadog
10
+ # Datadog Data Streams Monitoring public API.
11
+ #
12
+ # The Datadog team ensures that public methods in this module
13
+ # only receive backwards compatible changes, and breaking changes
14
+ # will only occur in new major versions releases.
15
+ # @public_api
16
+ module DataStreams
17
+ class << self
18
+ # Set a produce checkpoint for Data Streams Monitoring
19
+ #
20
+ # @param type [String] The type of the checkpoint (e.g., 'kafka', 'kinesis', 'sns')
21
+ # @param destination [String] The destination (e.g., topic, exchange, stream name)
22
+ # @param auto_instrumentation [Boolean] Whether this checkpoint was set by auto-instrumentation (default: false)
23
+ # @param tags [Hash] Additional tags to include
24
+ # @yield [key, value] Block to inject context into carrier
25
+ # @return [String, nil] Base64 encoded pathway context or nil if disabled
26
+ # @public_api
27
+ def set_produce_checkpoint(type:, destination:, auto_instrumentation: false, tags: {}, &block)
28
+ processor&.set_produce_checkpoint(
29
+ type: type,
30
+ destination: destination,
31
+ manual_checkpoint: !auto_instrumentation,
32
+ tags: tags,
33
+ &block
34
+ )
35
+ end
36
+
37
+ # Set a consume checkpoint for Data Streams Monitoring
38
+ #
39
+ # @param type [String] The type of the checkpoint (e.g., 'kafka', 'kinesis', 'sns')
40
+ # @param source [String] The source (e.g., topic, exchange, stream name)
41
+ # @param auto_instrumentation [Boolean] Whether this checkpoint was set by auto-instrumentation (default: false)
42
+ # @param tags [Hash] Additional tags to include
43
+ # @yield [key] Block to extract context from carrier
44
+ # @return [String, nil] Base64 encoded pathway context or nil if disabled
45
+ # @public_api
46
+ def set_consume_checkpoint(type:, source:, auto_instrumentation: false, tags: {}, &block)
47
+ processor&.set_consume_checkpoint(
48
+ type: type,
49
+ source: source,
50
+ manual_checkpoint: !auto_instrumentation,
51
+ tags: tags,
52
+ &block
53
+ )
54
+ end
55
+
56
+ # Track Kafka produce offset for lag monitoring
57
+ #
58
+ # @param topic [String] The Kafka topic name
59
+ # @param partition [Integer] The partition number
60
+ # @param offset [Integer] The offset of the produced message
61
+ # @return [Boolean, nil] true if tracking succeeded, nil if disabled
62
+ # @!visibility private
63
+ def track_kafka_produce(topic, partition, offset)
64
+ processor&.track_kafka_produce(topic, partition, offset, Core::Utils::Time.now)
65
+ end
66
+
67
+ # Track Kafka message consumption for consumer lag monitoring
68
+ #
69
+ # @param topic [String] The Kafka topic name
70
+ # @param partition [Integer] The partition number
71
+ # @param offset [Integer] The offset of the consumed message
72
+ # @return [Boolean, nil] true if tracking succeeded, nil if disabled
73
+ # @!visibility private
74
+ def track_kafka_consume(topic, partition, offset)
75
+ processor&.track_kafka_consume(topic, partition, offset, Core::Utils::Time.now)
76
+ end
77
+
78
+ # Check if Data Streams Monitoring is enabled and available
79
+ #
80
+ # @return [Boolean] true if the processor is available
81
+ # @public_api
82
+ def enabled?
83
+ !processor.nil?
84
+ end
85
+
86
+ private
87
+
88
+ def processor
89
+ components.data_streams
90
+ end
91
+
92
+ def components
93
+ Datadog.send(:components)
94
+ end
95
+ end
96
+
97
+ # Expose Data Streams to global shared objects
98
+ Extensions.activate!
99
+ end
100
+ end
@@ -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'
@@ -29,22 +29,6 @@ module Datadog
29
29
  end
30
30
  end
31
31
 
32
- def build!(settings, agent_settings, logger, telemetry: nil)
33
- unless settings.respond_to?(:dynamic_instrumentation) && settings.dynamic_instrumentation.enabled
34
- raise "Requested DI component but DI is not enabled in settings"
35
- end
36
-
37
- unless settings.respond_to?(:remote) && settings.remote.enabled
38
- raise "Requested DI component but remote config is not enabled in settings"
39
- end
40
-
41
- unless environment_supported?(settings, logger)
42
- raise "DI does not support the environment (development or Ruby version too low or not MRI)"
43
- end
44
-
45
- new(settings, agent_settings, logger, code_tracker: DI.code_tracker, telemetry: telemetry)
46
- end
47
-
48
32
  # Checks whether the runtime environment is supported by
49
33
  # dynamic instrumentation. Currently we only require that, if Rails
50
34
  # is used, that Rails environment is not development because
@@ -115,6 +99,20 @@ module Datadog
115
99
 
116
100
  def parse_probe_spec_and_notify(probe_spec)
117
101
  probe = ProbeBuilder.build_from_remote_config(probe_spec)
102
+ rescue => exc
103
+ begin
104
+ probe = Struct.new(:id).new(
105
+ probe_spec['id'],
106
+ )
107
+ payload = probe_notification_builder.build_errored(probe, exc)
108
+ probe_notifier_worker.add_status(payload)
109
+ rescue # standard:disable Lint/UselessRescue
110
+ # TODO report via instrumentation telemetry?
111
+ raise
112
+ end
113
+
114
+ raise
115
+ else
118
116
  payload = probe_notification_builder.build_received(probe)
119
117
  probe_notifier_worker.add_status(payload)
120
118
  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
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # Evaluator for expression language.
7
+ #
8
+ # @api private
9
+ class Evaluator
10
+ def ref(var)
11
+ @context.fetch(var)
12
+ end
13
+
14
+ def iref(var)
15
+ @context.fetch_ivar(var)
16
+ end
17
+
18
+ def len(var, var_name)
19
+ case var
20
+ when Array, String, Hash
21
+ var.length
22
+ else
23
+ raise DI::Error::ExpressionEvaluationError, "Unsupported type for length: #{var.class}: #{var_name}"
24
+ end
25
+ end
26
+
27
+ def is_empty(var, var_name)
28
+ case var
29
+ when nil, Numeric
30
+ false
31
+ when Array, String
32
+ var.empty?
33
+ else
34
+ raise DI::Error::ExpressionEvaluationError, "Unsupported type for isEmpty: #{var.class}: #{var_name}"
35
+ end
36
+ end
37
+
38
+ def is_undefined(var, var_name)
39
+ var.nil?
40
+ end
41
+
42
+ def contains(haystack, needle)
43
+ if String === haystack && String === needle or # standard:disable Style/AndOr
44
+ Array === haystack
45
+ haystack.include?(needle)
46
+ else
47
+ raise DI::Error::ExpressionEvaluationError, "Invalid arguments for contains: #{haystack}, #{needle}"
48
+ end
49
+ end
50
+
51
+ def matches(haystack, needle)
52
+ re = Regexp.compile(needle)
53
+ !!(haystack =~ re)
54
+ end
55
+
56
+ def getmember(object, field)
57
+ object.instance_variable_get("@#{field}")
58
+ end
59
+
60
+ def index(array_or_hash, index_or_key)
61
+ case array_or_hash
62
+ when Array
63
+ case index_or_key
64
+ when Integer
65
+ array_or_hash[index_or_key]
66
+ else
67
+ raise DI::Error::ExpressionEvaluationError, "Invalid index value: #{index_or_key}"
68
+ end
69
+ when Hash
70
+ array_or_hash[index_or_key]
71
+ else
72
+ raise DI::Error::ExpressionEvaluationError, "Invalid argument for index: #{array_or_hash}"
73
+ end
74
+ end
75
+
76
+ def substring(object, from, to)
77
+ unless String === object
78
+ raise DI::Error::ExpressionEvaluationError, "Invalid type for substring: #{object}"
79
+ end
80
+ object[from...to]
81
+ end
82
+
83
+ def starts_with(haystack, needle)
84
+ # To guard against running arbitrary customer code, check that
85
+ # the haystack is a string. This does not help if customer
86
+ # overrode String#start_with? but at least it's better than nothing.
87
+ String === haystack && haystack.start_with?(needle)
88
+ end
89
+
90
+ def ends_with(haystack, needle)
91
+ String === haystack && haystack.end_with?(needle)
92
+ end
93
+
94
+ def all(collection, &block)
95
+ case collection
96
+ when Array
97
+ collection.all? do |item|
98
+ block.call(item)
99
+ end
100
+ when Hash
101
+ # For hashes, the expression language has both @it and
102
+ # @key/@value. Manufacture @it from the key and value.
103
+ collection.all? do |key, value|
104
+ block.call([key, value], key, value)
105
+ end
106
+ else
107
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for all: #{collection.class}"
108
+ end
109
+ end
110
+
111
+ def any(collection, &block)
112
+ case collection
113
+ when Array
114
+ collection.any? do |item|
115
+ block.call(item)
116
+ end
117
+ when Hash
118
+ collection.any? do |key, value|
119
+ # For hashes, the expression language has both @it and
120
+ # @key/@value. Manufacture @it from the key and value.
121
+ block.call([key, value], key, value)
122
+ end
123
+ else
124
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for any: #{collection.class}"
125
+ end
126
+ end
127
+
128
+ def filter(collection, &block)
129
+ case collection
130
+ when Array
131
+ collection.select do |item|
132
+ block.call(item)
133
+ end
134
+ when Hash
135
+ collection.select do |key, value|
136
+ block.call([key, value], key, value)
137
+ end.to_h
138
+ else
139
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for filter: #{collection.class}"
140
+ end
141
+ end
142
+
143
+ def instanceof(object, cls_name)
144
+ cls = object.class
145
+ loop do
146
+ if cls.name == cls_name
147
+ return true
148
+ end
149
+ if supercls = cls.superclass # standard:disable Lint/AssignmentInCondition
150
+ cls = supercls
151
+ else
152
+ return false
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # Represents an Expression Language expression.
7
+ #
8
+ # @api private
9
+ class Expression
10
+ def initialize(dsl_expr, compiled_expr)
11
+ unless String === compiled_expr
12
+ raise ArgumentError, "compiled_expr must be a string"
13
+ end
14
+
15
+ @dsl_expr = dsl_expr
16
+
17
+ cls = Class.new(Evaluator)
18
+ cls.class_exec do
19
+ eval(<<-RUBY, Object.new.send(:binding), __FILE__, __LINE__ + 1) # standard:disable Security/Eval
20
+ def evaluate(context)
21
+ @context = context
22
+ #{compiled_expr}
23
+ end
24
+ RUBY
25
+ end
26
+ @evaluator = cls.new
27
+ end
28
+
29
+ attr_reader :dsl_expr
30
+ attr_reader :evaluator
31
+
32
+ def evaluate(context)
33
+ @evaluator.evaluate(context)
34
+ end
35
+
36
+ def satisfied?(context)
37
+ !!evaluate(context)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'el/expression'
4
+ require_relative 'el/compiler'
5
+ require_relative 'el/evaluator'