datadog 2.7.1 → 2.9.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/ext/datadog_profiling_native_extension/clock_id.h +2 -2
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -54
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +16 -16
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +259 -132
  10. data/ext/datadog_profiling_native_extension/extconf.rb +0 -8
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +1 -1
  13. data/ext/datadog_profiling_native_extension/http_transport.c +4 -4
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +4 -1
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -1
  16. data/ext/datadog_profiling_native_extension/profiling.c +10 -8
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +8 -8
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +54 -88
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -1
  20. data/ext/datadog_profiling_native_extension/time_helpers.h +1 -1
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.c +47 -0
  22. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +31 -0
  23. data/ext/libdatadog_api/crashtracker.c +3 -0
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/assets/waf_rules/recommended.json +355 -157
  26. data/lib/datadog/appsec/assets/waf_rules/strict.json +62 -32
  27. data/lib/datadog/appsec/component.rb +1 -8
  28. data/lib/datadog/appsec/context.rb +54 -0
  29. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
  30. data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
  31. data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
  32. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +6 -6
  33. data/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +4 -4
  34. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +19 -28
  35. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +5 -5
  36. data/lib/datadog/appsec/contrib/rack/gateway/response.rb +3 -3
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +64 -96
  38. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +10 -10
  39. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +5 -5
  40. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +6 -6
  41. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +10 -11
  42. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +43 -49
  43. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +21 -32
  44. data/lib/datadog/appsec/contrib/rails/patcher.rb +1 -1
  45. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +6 -6
  46. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +41 -63
  47. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +2 -2
  48. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +5 -5
  49. data/lib/datadog/appsec/event.rb +6 -6
  50. data/lib/datadog/appsec/ext.rb +3 -1
  51. data/lib/datadog/appsec/monitor/gateway/watcher.rb +22 -32
  52. data/lib/datadog/appsec/monitor/reactive/set_user.rb +5 -5
  53. data/lib/datadog/appsec/processor/context.rb +2 -2
  54. data/lib/datadog/appsec/processor/rule_loader.rb +0 -3
  55. data/lib/datadog/appsec/remote.rb +1 -3
  56. data/lib/datadog/appsec/response.rb +7 -11
  57. data/lib/datadog/appsec.rb +6 -5
  58. data/lib/datadog/auto_instrument.rb +3 -0
  59. data/lib/datadog/core/configuration/agent_settings_resolver.rb +39 -11
  60. data/lib/datadog/core/configuration/components.rb +20 -2
  61. data/lib/datadog/core/configuration/settings.rb +10 -0
  62. data/lib/datadog/core/configuration.rb +10 -2
  63. data/lib/datadog/{tracing → core}/contrib/rails/utils.rb +1 -3
  64. data/lib/datadog/core/crashtracking/component.rb +1 -3
  65. data/lib/datadog/core/remote/client/capabilities.rb +6 -0
  66. data/lib/datadog/core/remote/client.rb +65 -59
  67. data/lib/datadog/core/telemetry/component.rb +9 -3
  68. data/lib/datadog/core/telemetry/event.rb +87 -3
  69. data/lib/datadog/core/telemetry/ext.rb +1 -0
  70. data/lib/datadog/core/telemetry/logging.rb +2 -2
  71. data/lib/datadog/core/telemetry/metric.rb +22 -0
  72. data/lib/datadog/core/telemetry/worker.rb +33 -0
  73. data/lib/datadog/di/base.rb +115 -0
  74. data/lib/datadog/di/code_tracker.rb +11 -7
  75. data/lib/datadog/di/component.rb +21 -11
  76. data/lib/datadog/di/configuration/settings.rb +11 -1
  77. data/lib/datadog/di/contrib/active_record.rb +1 -0
  78. data/lib/datadog/di/contrib/railtie.rb +15 -0
  79. data/lib/datadog/di/contrib.rb +26 -0
  80. data/lib/datadog/di/error.rb +5 -0
  81. data/lib/datadog/di/instrumenter.rb +111 -20
  82. data/lib/datadog/di/preload.rb +18 -0
  83. data/lib/datadog/di/probe.rb +11 -1
  84. data/lib/datadog/di/probe_builder.rb +1 -0
  85. data/lib/datadog/di/probe_manager.rb +8 -5
  86. data/lib/datadog/di/probe_notification_builder.rb +27 -7
  87. data/lib/datadog/di/probe_notifier_worker.rb +5 -6
  88. data/lib/datadog/di/remote.rb +124 -0
  89. data/lib/datadog/di/serializer.rb +14 -7
  90. data/lib/datadog/di/transport.rb +3 -5
  91. data/lib/datadog/di/utils.rb +7 -0
  92. data/lib/datadog/di.rb +23 -62
  93. data/lib/datadog/kit/appsec/events.rb +3 -3
  94. data/lib/datadog/kit/identity.rb +4 -4
  95. data/lib/datadog/profiling/component.rb +59 -69
  96. data/lib/datadog/profiling/http_transport.rb +1 -26
  97. data/lib/datadog/tracing/configuration/settings.rb +4 -8
  98. data/lib/datadog/tracing/contrib/action_cable/integration.rb +5 -2
  99. data/lib/datadog/tracing/contrib/action_mailer/integration.rb +6 -2
  100. data/lib/datadog/tracing/contrib/action_pack/integration.rb +5 -2
  101. data/lib/datadog/tracing/contrib/action_view/integration.rb +5 -2
  102. data/lib/datadog/tracing/contrib/active_job/integration.rb +5 -2
  103. data/lib/datadog/tracing/contrib/active_record/integration.rb +6 -2
  104. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +3 -1
  105. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +3 -1
  106. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
  107. data/lib/datadog/tracing/contrib/active_support/configuration/settings.rb +10 -0
  108. data/lib/datadog/tracing/contrib/active_support/integration.rb +5 -2
  109. data/lib/datadog/tracing/contrib/auto_instrument.rb +2 -2
  110. data/lib/datadog/tracing/contrib/aws/integration.rb +3 -0
  111. data/lib/datadog/tracing/contrib/concurrent_ruby/integration.rb +3 -0
  112. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
  113. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  114. data/lib/datadog/tracing/contrib/httprb/integration.rb +3 -0
  115. data/lib/datadog/tracing/contrib/kafka/integration.rb +3 -0
  116. data/lib/datadog/tracing/contrib/mongodb/integration.rb +3 -0
  117. data/lib/datadog/tracing/contrib/opensearch/integration.rb +3 -0
  118. data/lib/datadog/tracing/contrib/presto/integration.rb +3 -0
  119. data/lib/datadog/tracing/contrib/rack/integration.rb +2 -2
  120. data/lib/datadog/tracing/contrib/rails/framework.rb +2 -2
  121. data/lib/datadog/tracing/contrib/rails/patcher.rb +1 -1
  122. data/lib/datadog/tracing/contrib/rest_client/integration.rb +3 -0
  123. data/lib/datadog/tracing/span.rb +12 -4
  124. data/lib/datadog/tracing/span_event.rb +123 -3
  125. data/lib/datadog/tracing/span_operation.rb +6 -0
  126. data/lib/datadog/tracing/transport/serializable_trace.rb +24 -6
  127. data/lib/datadog/version.rb +2 -2
  128. data/lib/datadog.rb +3 -0
  129. metadata +30 -17
  130. data/lib/datadog/appsec/processor/actions.rb +0 -49
  131. data/lib/datadog/appsec/reactive/operation.rb +0 -68
  132. data/lib/datadog/appsec/scope.rb +0 -58
  133. data/lib/datadog/core/crashtracking/agent_base_url.rb +0 -21
@@ -863,6 +863,16 @@ module Datadog
863
863
  o.type :float
864
864
  o.default 1.0
865
865
  end
866
+
867
+ # Enable log collection for telemetry. Log collection only works when telemetry is enabled and
868
+ # logs are enabled.
869
+ # @default `DD_TELEMETRY_LOG_COLLECTION_ENABLED` environment variable, otherwise `true`.
870
+ # @return [Boolean]
871
+ option :log_collection_enabled do |o|
872
+ o.type :bool
873
+ o.env Core::Telemetry::Ext::ENV_LOG_COLLECTION
874
+ o.default true
875
+ end
866
876
  end
867
877
 
868
878
  # Remote configuration
@@ -236,7 +236,7 @@ module Datadog
236
236
  rescue ThreadError => e
237
237
  logger_without_components.error(
238
238
  'Detected deadlock during datadog initialization. ' \
239
- 'Please report this at https://github.com/DataDog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug' \
239
+ 'Please report this at https://github.com/datadog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug' \
240
240
  "\n\tSource:\n\t#{Array(e.backtrace).join("\n\t")}"
241
241
  )
242
242
  nil
@@ -258,8 +258,16 @@ module Datadog
258
258
  def replace_components!(settings, old)
259
259
  components = Components.new(settings)
260
260
 
261
+ # Carry over state from existing components to the new ones.
262
+ # Currently, if we already started the remote component (which
263
+ # happens after a request goes through installed Rack middleware),
264
+ # we will start the new remote component as well.
265
+ old_state = {
266
+ remote_started: old.remote&.started?,
267
+ }
268
+
261
269
  old.shutdown!(components)
262
- components.startup!(settings)
270
+ components.startup!(settings, old_state: old_state)
263
271
  components
264
272
  end
265
273
 
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../analytics'
4
-
5
3
  module Datadog
6
- module Tracing
4
+ module Core
7
5
  module Contrib
8
6
  module Rails
9
7
  # common utilities for Rails
@@ -3,7 +3,6 @@
3
3
  require 'libdatadog'
4
4
 
5
5
  require_relative 'tag_builder'
6
- require_relative 'agent_base_url'
7
6
  require_relative '../utils/only_once'
8
7
  require_relative '../utils/at_fork_monkey_patch'
9
8
 
@@ -31,8 +30,7 @@ module Datadog
31
30
 
32
31
  def self.build(settings, agent_settings, logger:)
33
32
  tags = TagBuilder.call(settings)
34
- agent_base_url = AgentBaseUrl.resolve(agent_settings)
35
- logger.warn('Missing agent base URL; cannot enable crash tracking') unless agent_base_url
33
+ agent_base_url = agent_settings.url
36
34
 
37
35
  ld_library_path = ::Libdatadog.ld_library_path
38
36
  logger.warn('Missing ld_library_path; cannot enable crash tracking') unless ld_library_path
@@ -32,6 +32,12 @@ module Datadog
32
32
  register_receivers(Datadog::AppSec::Remote.receivers(@telemetry))
33
33
  end
34
34
 
35
+ if settings.respond_to?(:dynamic_instrumentation) && settings.dynamic_instrumentation.enabled
36
+ register_capabilities(Datadog::DI::Remote.capabilities)
37
+ register_products(Datadog::DI::Remote.products)
38
+ register_receivers(Datadog::DI::Remote.receivers(@telemetry))
39
+ end
40
+
35
41
  register_capabilities(Datadog::Tracing::Remote.capabilities)
36
42
  register_products(Datadog::Tracing::Remote.products)
37
43
  register_receivers(Datadog::Tracing::Remote.receivers(@telemetry))
@@ -24,93 +24,99 @@ module Datadog
24
24
  @dispatcher = Dispatcher.new(@capabilities.receivers)
25
25
  end
26
26
 
27
- # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity
28
27
  def sync
29
28
  # TODO: Skip sync if no capabilities are registered
30
29
  response = transport.send_config(payload)
31
30
 
32
31
  if response.ok?
33
- # when response is completely empty, do nothing as in: leave as is
34
- if response.empty?
35
- Datadog.logger.debug { 'remote: empty response => NOOP' }
32
+ process_response(response)
33
+ elsif response.internal_error?
34
+ raise TransportError, response.to_s
35
+ end
36
+ end
36
37
 
37
- return
38
- end
38
+ private
39
39
 
40
- begin
41
- paths = response.client_configs.map do |path|
42
- Configuration::Path.parse(path)
43
- end
40
+ def process_response(response)
41
+ # when response is completely empty, do nothing as in: leave as is
42
+ if response.empty?
43
+ Datadog.logger.debug { 'remote: empty response => NOOP' }
44
44
 
45
- targets = Configuration::TargetMap.parse(response.targets)
45
+ return
46
+ end
46
47
 
47
- contents = Configuration::ContentList.parse(response.target_files)
48
- rescue Remote::Configuration::Path::ParseError => e
49
- raise SyncError, e.message
48
+ begin
49
+ paths = response.client_configs.map do |path|
50
+ Configuration::Path.parse(path)
50
51
  end
51
52
 
52
- # To make sure steep does not complain
53
- return unless paths && targets && contents
53
+ targets = Configuration::TargetMap.parse(response.targets)
54
+
55
+ contents = Configuration::ContentList.parse(response.target_files)
56
+ rescue Remote::Configuration::Path::ParseError => e
57
+ raise SyncError, e.message
58
+ end
54
59
 
55
- # TODO: sometimes it can strangely be so that paths.empty?
56
- # TODO: sometimes it can strangely be so that targets.empty?
60
+ # To make sure steep does not complain
61
+ return unless paths && targets && contents
57
62
 
58
- changes = repository.transaction do |current, transaction|
59
- # paths to be removed: previously applied paths minus ingress paths
60
- (current.paths - paths).each { |p| transaction.delete(p) }
63
+ # TODO: sometimes it can strangely be so that paths.empty?
64
+ # TODO: sometimes it can strangely be so that targets.empty?
61
65
 
62
- # go through each ingress path
63
- paths.each do |path|
64
- # match target with path
65
- target = targets[path]
66
+ apply_config(paths, targets, contents)
67
+ end
66
68
 
67
- # abort entirely if matching target not found
68
- raise SyncError, "no target for path '#{path}'" if target.nil?
69
+ def apply_config(paths, targets, contents)
70
+ changes = repository.transaction do |current, transaction|
71
+ # paths to be removed: previously applied paths minus ingress paths
72
+ (current.paths - paths).each { |p| transaction.delete(p) }
69
73
 
70
- # new paths are not in previously applied paths
71
- new = !current.paths.include?(path)
74
+ # go through each ingress path
75
+ paths.each do |path|
76
+ # match target with path
77
+ target = targets[path]
72
78
 
73
- # updated paths are in previously applied paths
74
- # but the content hash changed
75
- changed = current.paths.include?(path) && !current.contents.find_content(path, target)
79
+ # abort entirely if matching target not found
80
+ raise SyncError, "no target for path '#{path}'" if target.nil?
76
81
 
77
- # skip if unchanged
78
- same = !new && !changed
82
+ # new paths are not in previously applied paths
83
+ new = !current.paths.include?(path)
79
84
 
80
- next if same
85
+ # updated paths are in previously applied paths
86
+ # but the content hash changed
87
+ changed = current.paths.include?(path) && !current.contents.find_content(path, target)
81
88
 
82
- # match content with path and target
83
- content = contents.find_content(path, target)
89
+ # skip if unchanged
90
+ same = !new && !changed
84
91
 
85
- # abort entirely if matching content not found
86
- raise SyncError, "no valid content for target at path '#{path}'" if content.nil?
92
+ next if same
87
93
 
88
- # to be added or updated << config
89
- # TODO: metadata (hash, version, etc...)
90
- transaction.insert(path, target, content) if new
91
- transaction.update(path, target, content) if changed
92
- end
94
+ # match content with path and target
95
+ content = contents.find_content(path, target)
93
96
 
94
- # save backend opaque backend state
95
- transaction.set(opaque_backend_state: targets.opaque_backend_state)
96
- transaction.set(targets_version: targets.version)
97
+ # abort entirely if matching content not found
98
+ raise SyncError, "no valid content for target at path '#{path}'" if content.nil?
97
99
 
98
- # upon transaction end, new list of applied config + metadata (add, change, remove) will be saved
99
- # TODO: also remove stale config (matching removed) from cache (client configs is exhaustive list of paths)
100
+ # to be added or updated << config
101
+ # TODO: metadata (hash, version, etc...)
102
+ transaction.insert(path, target, content) if new
103
+ transaction.update(path, target, content) if changed
100
104
  end
101
105
 
102
- if changes.empty?
103
- Datadog.logger.debug { 'remote: no changes' }
104
- else
105
- dispatcher.dispatch(changes, repository)
106
- end
107
- elsif response.internal_error?
108
- raise TransportError, response.to_s
106
+ # save backend opaque backend state
107
+ transaction.set(opaque_backend_state: targets.opaque_backend_state)
108
+ transaction.set(targets_version: targets.version)
109
+
110
+ # upon transaction end, new list of applied config + metadata (add, change, remove) will be saved
111
+ # TODO: also remove stale config (matching removed) from cache (client configs is exhaustive list of paths)
109
112
  end
110
- end
111
- # rubocop:enable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity
112
113
 
113
- private
114
+ if changes.empty?
115
+ Datadog.logger.debug { 'remote: no changes' }
116
+ else
117
+ dispatcher.dispatch(changes, repository)
118
+ end
119
+ end
114
120
 
115
121
  def payload # rubocop:disable Metrics/MethodLength
116
122
  state = repository.state
@@ -14,6 +14,7 @@ module Datadog
14
14
  module Core
15
15
  module Telemetry
16
16
  # Telemetry entrypoint, coordinates sending telemetry events at various points in app lifecycle.
17
+ # Note: Telemetry does not spawn its worker thread in fork processes, thus no telemetry is sent in forked processes.
17
18
  class Component
18
19
  attr_reader :enabled
19
20
 
@@ -52,6 +53,7 @@ module Datadog
52
53
  metrics_aggregation_interval_seconds: settings.telemetry.metrics_aggregation_interval_seconds,
53
54
  dependency_collection: settings.telemetry.dependency_collection,
54
55
  shutdown_timeout_seconds: settings.telemetry.shutdown_timeout_seconds,
56
+ log_collection_enabled: settings.telemetry.log_collection_enabled
55
57
  )
56
58
  end
57
59
 
@@ -67,10 +69,11 @@ module Datadog
67
69
  http_transport:,
68
70
  shutdown_timeout_seconds:,
69
71
  enabled: true,
70
- metrics_enabled: true
72
+ metrics_enabled: true,
73
+ log_collection_enabled: true
71
74
  )
72
75
  @enabled = enabled
73
- @stopped = false
76
+ @log_collection_enabled = log_collection_enabled
74
77
 
75
78
  @metrics_manager = MetricsManager.new(
76
79
  enabled: enabled && metrics_enabled,
@@ -86,6 +89,9 @@ module Datadog
86
89
  dependency_collection: dependency_collection,
87
90
  shutdown_timeout: shutdown_timeout_seconds
88
91
  )
92
+
93
+ @stopped = false
94
+
89
95
  @worker.start
90
96
  end
91
97
 
@@ -114,7 +120,7 @@ module Datadog
114
120
  end
115
121
 
116
122
  def log!(event)
117
- return unless @enabled || forked?
123
+ return if !@enabled || forked? || !@log_collection_enabled
118
124
 
119
125
  @worker.enqueue(event)
120
126
  end
@@ -29,6 +29,22 @@ module Datadog
29
29
  def payload
30
30
  {}
31
31
  end
32
+
33
+ # Override equality to allow for deduplication
34
+ # The basic implementation is to check if the other object is an instance of the same class.
35
+ # This works for events that have no attributes.
36
+ # For events with attributes, you should override this method to compare the attributes.
37
+ def ==(other)
38
+ other.is_a?(self.class)
39
+ end
40
+
41
+ # @see #==
42
+ alias eql? ==
43
+
44
+ # @see #==
45
+ def hash
46
+ self.class.hash
47
+ end
32
48
  end
33
49
 
34
50
  # Telemetry class for the 'app-started' event
@@ -263,6 +279,8 @@ module Datadog
263
279
 
264
280
  # Telemetry class for the 'app-client-configuration-change' event
265
281
  class AppClientConfigurationChange < Base
282
+ attr_reader :changes, :origin
283
+
266
284
  def type
267
285
  'app-client-configuration-change'
268
286
  end
@@ -301,6 +319,16 @@ module Datadog
301
319
 
302
320
  res
303
321
  end
322
+
323
+ def ==(other)
324
+ other.is_a?(AppClientConfigurationChange) && other.changes == @changes && other.origin == @origin
325
+ end
326
+
327
+ alias eql? ==
328
+
329
+ def hash
330
+ [self.class, @changes, @origin].hash
331
+ end
304
332
  end
305
333
 
306
334
  # Telemetry class for the 'app-heartbeat' event
@@ -319,6 +347,8 @@ module Datadog
319
347
 
320
348
  # Telemetry class for the 'generate-metrics' event
321
349
  class GenerateMetrics < Base
350
+ attr_reader :namespace, :metric_series
351
+
322
352
  def type
323
353
  'generate-metrics'
324
354
  end
@@ -335,24 +365,54 @@ module Datadog
335
365
  series: @metric_series.map(&:to_h)
336
366
  }
337
367
  end
368
+
369
+ def ==(other)
370
+ other.is_a?(GenerateMetrics) && other.namespace == @namespace && other.metric_series == @metric_series
371
+ end
372
+
373
+ alias eql? ==
374
+
375
+ def hash
376
+ [self.class, @namespace, @metric_series].hash
377
+ end
338
378
  end
339
379
 
340
- # Telemetry class for the 'logs' event
380
+ # Telemetry class for the 'logs' event.
381
+ # Logs with the same content are deduplicated at flush time.
341
382
  class Log < Base
342
383
  LEVELS = {
343
384
  error: 'ERROR',
344
385
  warn: 'WARN',
345
386
  }.freeze
346
387
 
388
+ LEVELS_STRING = LEVELS.values.freeze
389
+
390
+ attr_reader :message, :level, :stack_trace, :count
391
+
347
392
  def type
348
393
  'logs'
349
394
  end
350
395
 
351
- def initialize(message:, level:, stack_trace: nil)
396
+ # @param message [String] the log message
397
+ # @param level [Symbol, String] the log level. Either :error, :warn, 'ERROR', or 'WARN'.
398
+ # @param stack_trace [String, nil] the stack trace
399
+ # @param count [Integer] the number of times the log was emitted. Used for deduplication.
400
+ def initialize(message:, level:, stack_trace: nil, count: 1)
352
401
  super()
353
402
  @message = message
354
403
  @stack_trace = stack_trace
355
- @level = LEVELS.fetch(level) { |k| raise ArgumentError, "Invalid log level :#{k}" }
404
+
405
+ if level.is_a?(String) && LEVELS_STRING.include?(level)
406
+ # String level is used during object copy for deduplication
407
+ @level = level
408
+ elsif level.is_a?(Symbol)
409
+ # Symbol level is used by the regular log emitter user
410
+ @level = LEVELS.fetch(level) { |k| raise ArgumentError, "Invalid log level :#{k}" }
411
+ else
412
+ raise ArgumentError, "Invalid log level #{level}"
413
+ end
414
+
415
+ @count = count
356
416
  end
357
417
 
358
418
  def payload
@@ -362,10 +422,24 @@ module Datadog
362
422
  message: @message,
363
423
  level: @level,
364
424
  stack_trace: @stack_trace,
425
+ count: @count,
365
426
  }.compact
366
427
  ]
367
428
  }
368
429
  end
430
+
431
+ # override equality to allow for deduplication
432
+ def ==(other)
433
+ other.is_a?(Log) &&
434
+ other.message == @message &&
435
+ other.level == @level && other.stack_trace == @stack_trace && other.count == @count
436
+ end
437
+
438
+ alias eql? ==
439
+
440
+ def hash
441
+ [self.class, @message, @level, @stack_trace, @count].hash
442
+ end
369
443
  end
370
444
 
371
445
  # Telemetry class for the 'distributions' event
@@ -395,6 +469,16 @@ module Datadog
395
469
  }
396
470
  end
397
471
  end
472
+
473
+ def ==(other)
474
+ other.is_a?(MessageBatch) && other.events == @events
475
+ end
476
+
477
+ alias eql? ==
478
+
479
+ def hash
480
+ [self.class, @events].hash
481
+ end
398
482
  end
399
483
  end
400
484
  end
@@ -13,6 +13,7 @@ module Datadog
13
13
  ENV_INSTALL_TYPE = 'DD_INSTRUMENTATION_INSTALL_TYPE'
14
14
  ENV_INSTALL_TIME = 'DD_INSTRUMENTATION_INSTALL_TIME'
15
15
  ENV_AGENTLESS_URL_OVERRIDE = 'DD_TELEMETRY_AGENTLESS_URL'
16
+ ENV_LOG_COLLECTION = 'DD_TELEMETRY_LOG_COLLECTION_ENABLED'
16
17
  end
17
18
  end
18
19
  end
@@ -41,7 +41,7 @@ module Datadog
41
41
  else
42
42
  'REDACTED'
43
43
  end
44
- end.join(',')
44
+ end.join("\n")
45
45
  end
46
46
  end
47
47
 
@@ -49,7 +49,7 @@ module Datadog
49
49
  # Annoymous exceptions to be logged as <Class:0x00007f8b1c0b3b40>
50
50
  message = +''
51
51
  message << (exception.class.name || exception.class.inspect)
52
- message << ':' << description if description
52
+ message << ': ' << description if description
53
53
 
54
54
  event = Event::Log.new(
55
55
  message: message,
@@ -41,6 +41,18 @@ module Datadog
41
41
  }
42
42
  end
43
43
 
44
+ def ==(other)
45
+ other.is_a?(self.class) &&
46
+ name == other.name &&
47
+ values == other.values && tags == other.tags && common == other.common && type == other.type
48
+ end
49
+
50
+ alias eql? ==
51
+
52
+ def hash
53
+ [self.class, name, values, tags, common, type].hash
54
+ end
55
+
44
56
  private
45
57
 
46
58
  def tags_to_array(tags)
@@ -71,6 +83,16 @@ module Datadog
71
83
  res[:interval] = interval
72
84
  res
73
85
  end
86
+
87
+ def ==(other)
88
+ super && interval == other.interval
89
+ end
90
+
91
+ alias eql? ==
92
+
93
+ def hash
94
+ [super, interval].hash
95
+ end
74
96
  end
75
97
 
76
98
  # Count metric adds up all the submitted values in a time interval. This would be suitable for a
@@ -97,6 +97,8 @@ module Datadog
97
97
  return if events.empty?
98
98
  return if !enabled? || !sent_started_event?
99
99
 
100
+ events = deduplicate_logs(events)
101
+
100
102
  Datadog.logger.debug { "Sending #{events&.count} telemetry events" }
101
103
  send_event(Event::MessageBatch.new(events))
102
104
  end
@@ -167,6 +169,37 @@ module Datadog
167
169
  Datadog.logger.debug('Agent does not support telemetry; disabling future telemetry events.')
168
170
  disable!
169
171
  end
172
+
173
+ # Deduplicate logs by counting the number of repeated occurrences of the same log
174
+ # entry and replacing them with a single entry with the calculated `count` value.
175
+ # Non-log events are unchanged.
176
+ def deduplicate_logs(events)
177
+ return events if events.empty?
178
+
179
+ all_logs = []
180
+ other_events = events.reject do |event|
181
+ if event.is_a?(Event::Log)
182
+ all_logs << event
183
+ true
184
+ else
185
+ false
186
+ end
187
+ end
188
+
189
+ return events if all_logs.empty?
190
+
191
+ uniq_logs = all_logs.group_by(&:itself).map do |_, logs|
192
+ log = logs.first
193
+ if logs.size > 1
194
+ # New log event with a count of repeated occurrences
195
+ Event::Log.new(message: log.message, level: log.level, stack_trace: log.stack_trace, count: logs.size)
196
+ else
197
+ log
198
+ end
199
+ end
200
+
201
+ other_events + uniq_logs
202
+ end
170
203
  end
171
204
  end
172
205
  end