datadog 2.7.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
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