ddtrace 0.7.2 → 0.8.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 (37) hide show
  1. checksums.yaml +5 -13
  2. data/Appraisals +38 -19
  3. data/README.md +13 -0
  4. data/Rakefile +9 -9
  5. data/circle.yml +14 -11
  6. data/ddtrace.gemspec +1 -0
  7. data/docs/GettingStarted.md +96 -6
  8. data/gemfiles/rails30_postgres.gemfile +10 -0
  9. data/gemfiles/rails30_postgres_sidekiq.gemfile +11 -0
  10. data/gemfiles/{rails3_mysql2.gemfile → rails32_mysql2.gemfile} +0 -0
  11. data/gemfiles/{rails3_postgres.gemfile → rails32_postgres.gemfile} +0 -0
  12. data/gemfiles/{rails3_postgres_redis.gemfile → rails32_postgres_redis.gemfile} +0 -0
  13. data/gemfiles/{rails3_postgres_sidekiq.gemfile → rails32_postgres_sidekiq.gemfile} +1 -1
  14. data/lib/ddtrace/buffer.rb +1 -26
  15. data/lib/ddtrace/context.rb +145 -0
  16. data/lib/ddtrace/contrib/active_record/patcher.rb +2 -2
  17. data/lib/ddtrace/contrib/elasticsearch/patcher.rb +1 -0
  18. data/lib/ddtrace/contrib/grape/endpoint.rb +4 -4
  19. data/lib/ddtrace/contrib/http/patcher.rb +36 -9
  20. data/lib/ddtrace/contrib/rack/middlewares.rb +53 -12
  21. data/lib/ddtrace/contrib/rails/action_controller.rb +14 -5
  22. data/lib/ddtrace/contrib/rails/action_view.rb +3 -3
  23. data/lib/ddtrace/contrib/rails/active_record.rb +1 -1
  24. data/lib/ddtrace/contrib/rails/active_support.rb +2 -2
  25. data/lib/ddtrace/contrib/rails/core_extensions.rb +45 -20
  26. data/lib/ddtrace/contrib/rails/framework.rb +11 -0
  27. data/lib/ddtrace/distributed.rb +38 -0
  28. data/lib/ddtrace/ext/distributed.rb +10 -0
  29. data/lib/ddtrace/pin.rb +6 -4
  30. data/lib/ddtrace/provider.rb +16 -0
  31. data/lib/ddtrace/span.rb +54 -22
  32. data/lib/ddtrace/tracer.rb +120 -64
  33. data/lib/ddtrace/utils.rb +9 -2
  34. data/lib/ddtrace/version.rb +2 -2
  35. data/lib/ddtrace/workers.rb +1 -2
  36. data/lib/ddtrace/writer.rb +0 -1
  37. metadata +43 -27
@@ -0,0 +1,145 @@
1
+ require 'thread'
2
+
3
+ module Datadog
4
+ # \Context is used to keep track of a hierarchy of spans for the current
5
+ # execution flow. During each logical execution, the same \Context is
6
+ # used to represent a single logical trace, even if the trace is built
7
+ # asynchronously.
8
+ #
9
+ # A single code execution may use multiple \Context if part of the execution
10
+ # must not be related to the current tracing. As example, a delayed job may
11
+ # compose a standalone trace instead of being related to the same trace that
12
+ # generates the job itself. On the other hand, if it's part of the same
13
+ # \Context, it will be related to the original trace.
14
+ #
15
+ # This data structure is thread-safe.
16
+ class Context
17
+ # Initialize a new thread-safe \Context.
18
+ def initialize
19
+ @mutex = Mutex.new
20
+ reset
21
+ end
22
+
23
+ def reset
24
+ @trace = []
25
+ @sampled = false
26
+ @finished_spans = 0
27
+ @current_span = nil
28
+ end
29
+
30
+ # Return the last active span that corresponds to the last inserted
31
+ # item in the trace list. This cannot be considered as the current active
32
+ # span in asynchronous environments, because some spans can be closed
33
+ # earlier while child spans still need to finish their traced execution.
34
+ def current_span
35
+ @mutex.synchronize do
36
+ return @current_span
37
+ end
38
+ end
39
+
40
+ # Add a span to the context trace list, keeping it as the last active span.
41
+ def add_span(span)
42
+ @mutex.synchronize do
43
+ @current_span = span
44
+ @sampled = span.sampled
45
+ @trace << span
46
+ span.context = self
47
+ end
48
+ end
49
+
50
+ # Mark a span as a finished, increasing the internal counter to prevent
51
+ # cycles inside _trace list.
52
+ def close_span(span)
53
+ @mutex.synchronize do
54
+ @finished_spans += 1
55
+ # Current span is only meaningful for linear tree-like traces,
56
+ # in other cases, this is just broken and one should rely
57
+ # on per-instrumentation code to retrieve handle parent/child relations.
58
+ @current_span = span.parent
59
+ return if span.tracer.nil?
60
+ return unless Datadog::Tracer.debug_logging
61
+ if span.parent.nil? && !check_finished_spans
62
+ opened_spans = @trace.length - @finished_spans
63
+ Datadog::Tracer.log.debug("root span #{span.name} closed but has #{opened_spans} unfinished spans:")
64
+ @trace.each do |s|
65
+ Datadog::Tracer.log.debug("unfinished span: #{s}") unless s.finished?
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # Returns if the trace for the current Context is finished or not.
72
+ # Low-level internal function, not thread-safe.
73
+ def check_finished_spans
74
+ @finished_spans > 0 && @trace.length == @finished_spans
75
+ end
76
+
77
+ # Returns if the trace for the current Context is finished or not. A \Context
78
+ # is considered finished if all spans in this context are finished.
79
+ def finished?
80
+ @mutex.synchronize do
81
+ return check_finished_spans
82
+ end
83
+ end
84
+
85
+ # Returns true if the context is sampled, that is, if it should be kept
86
+ # and sent to the trace agent.
87
+ def sampled?
88
+ @mutex.synchronize do
89
+ return @sampled
90
+ end
91
+ end
92
+
93
+ # Returns both the trace list generated in the current context and
94
+ # if the context is sampled or not. It returns nil, nil if the ``Context`` is
95
+ # not finished. If a trace is returned, the \Context will be reset so that it
96
+ # can be re-used immediately.
97
+ #
98
+ # This operation is thread-safe.
99
+ def get
100
+ @mutex.synchronize do
101
+ return nil, nil unless check_finished_spans
102
+
103
+ trace = @trace
104
+ sampled = @sampled
105
+ reset
106
+ return trace, sampled
107
+ end
108
+ end
109
+
110
+ # Return a string representation of the context.
111
+ def to_s
112
+ @mutex.synchronize do
113
+ # rubocop:disable Metrics/LineLength
114
+ "Context(trace.length:#{@trace.length},sampled:#{@sampled},finished_spans:#{@finished_spans},current_span:#{@current_span})"
115
+ end
116
+ end
117
+
118
+ private :reset
119
+ private :check_finished_spans
120
+ end
121
+
122
+ # ThreadLocalContext can be used as a tracer global reference to create
123
+ # a different \Context for each thread. In synchronous tracer, this
124
+ # is required to prevent multiple threads sharing the same \Context
125
+ # in different executions.
126
+ class ThreadLocalContext
127
+ # ThreadLocalContext can be used as a tracer global reference to create
128
+ # a different \Context for each thread. In synchronous tracer, this
129
+ # is required to prevent multiple threads sharing the same \Context
130
+ # in different executions.
131
+ def initialize
132
+ self.local = Datadog::Context.new
133
+ end
134
+
135
+ # Override the thread-local context with a new context.
136
+ def local=(ctx)
137
+ Thread.current[:datadog_context] = ctx
138
+ end
139
+
140
+ # Return the thread-local context.
141
+ def local
142
+ Thread.current[:datadog_context] ||= Datadog::Context.new
143
+ end
144
+ end
145
+ end
@@ -23,7 +23,7 @@ module Datadog
23
23
  patch_active_record()
24
24
 
25
25
  @patched = true
26
- rescue => e
26
+ rescue StandardError => e
27
27
  Datadog::Tracer.log.error("Unable to apply Active Record integration: #{e}")
28
28
  end
29
29
  end
@@ -84,7 +84,7 @@ module Datadog
84
84
  span.span_type = Datadog::Ext::SQL::TYPE
85
85
  span.set_tag('active_record.db.vendor', adapter_name)
86
86
  span.start_time = start
87
- span.finish_at(finish)
87
+ span.finish(finish)
88
88
  rescue StandardError => e
89
89
  Datadog::Tracer.log.error(e.message)
90
90
  end
@@ -90,6 +90,7 @@ module Datadog
90
90
  ensure
91
91
  # the call is still executed
92
92
  response = perform_request_without_datadog(*args)
93
+ span.set_tag('http.status_code', response.status)
93
94
  end
94
95
  end
95
96
  response
@@ -79,12 +79,12 @@ module Datadog
79
79
  # catch thrown exceptions
80
80
  span.set_error(payload[:exception_object]) unless payload[:exception_object].nil?
81
81
 
82
- # ovverride the current span with this notification values
82
+ # override the current span with this notification values
83
83
  span.set_tag('grape.route.endpoint', api_view)
84
84
  span.set_tag('grape.route.path', path)
85
85
  ensure
86
86
  span.start_time = start
87
- span.finish_at(finish)
87
+ span.finish(finish)
88
88
  end
89
89
  rescue StandardError => e
90
90
  Datadog::Tracer.log.error(e.message)
@@ -125,7 +125,7 @@ module Datadog
125
125
  span.set_error(payload[:exception_object]) unless payload[:exception_object].nil?
126
126
  ensure
127
127
  span.start_time = start
128
- span.finish_at(finish)
128
+ span.finish(finish)
129
129
  end
130
130
  rescue StandardError => e
131
131
  Datadog::Tracer.log.error(e.message)
@@ -153,7 +153,7 @@ module Datadog
153
153
  span.set_tag('grape.filter.type', type.to_s)
154
154
  ensure
155
155
  span.start_time = start
156
- span.finish_at(finish)
156
+ span.finish(finish)
157
157
  end
158
158
  rescue StandardError => e
159
159
  Datadog::Tracer.log.error(e.message)
@@ -12,6 +12,12 @@ module Datadog
12
12
  APP = 'net/http'.freeze
13
13
  SERVICE = 'net/http'.freeze
14
14
 
15
+ @distributed_tracing_enabled = false
16
+
17
+ class << self
18
+ attr_accessor :distributed_tracing_enabled
19
+ end
20
+
15
21
  module_function
16
22
 
17
23
  def should_skip_tracing?(req, address, port, transport, pin)
@@ -35,6 +41,13 @@ module Datadog
35
41
  false
36
42
  end
37
43
 
44
+ def should_skip_distributed_tracing?(pin)
45
+ unless pin.config.nil?
46
+ return !pin.config.fetch(:distributed_tracing_enabled, @distributed_tracing_enabled)
47
+ end
48
+ !@distributed_tracing_enabled
49
+ end
50
+
38
51
  # Patcher enables patching of 'net/http' module.
39
52
  # This is used in monkey.rb to automatically apply patches
40
53
  module Patcher
@@ -52,6 +65,7 @@ module Datadog
52
65
  require 'ddtrace/ext/app_types'
53
66
  require 'ddtrace/ext/http'
54
67
  require 'ddtrace/ext/net'
68
+ require 'ddtrace/ext/distributed'
55
69
 
56
70
  patch_http()
57
71
 
@@ -69,6 +83,8 @@ module Datadog
69
83
  end
70
84
 
71
85
  # rubocop:disable Metrics/MethodLength
86
+ # rubocop:disable Metrics/BlockLength
87
+ # rubocop:disable Metrics/AbcSize
72
88
  def patch_http
73
89
  ::Net::HTTP.class_eval do
74
90
  alias_method :initialize_without_datadog, :initialize
@@ -84,6 +100,7 @@ module Datadog
84
100
 
85
101
  alias_method :request_without_datadog, :request
86
102
  remove_method :request
103
+
87
104
  def request(req, body = nil, &block) # :yield: +response+
88
105
  pin = Datadog::Pin.get_from(self)
89
106
  return request_without_datadog(req, body, &block) unless pin && pin.tracer
@@ -93,15 +110,25 @@ module Datadog
93
110
  Datadog::Contrib::HTTP.should_skip_tracing?(req, @address, @port, transport, pin)
94
111
 
95
112
  pin.tracer.trace(NAME) do |span|
96
- span.service = pin.service
97
- span.span_type = Datadog::Ext::HTTP::TYPE
98
-
99
- span.resource = req.method
100
- # Using the method as a resource, as URL/path can trigger
101
- # a possibly infinite number of resources.
102
- span.set_tag(Datadog::Ext::HTTP::URL, req.path)
103
- span.set_tag(Datadog::Ext::HTTP::METHOD, req.method)
104
- response = request_without_datadog(req, body, &block)
113
+ begin
114
+ span.service = pin.service
115
+ span.span_type = Datadog::Ext::HTTP::TYPE
116
+
117
+ span.resource = req.method
118
+ # Using the method as a resource, as URL/path can trigger
119
+ # a possibly infinite number of resources.
120
+ span.set_tag(Datadog::Ext::HTTP::URL, req.path)
121
+ span.set_tag(Datadog::Ext::HTTP::METHOD, req.method)
122
+
123
+ unless Datadog::Contrib::HTTP.should_skip_distributed_tracing?(pin)
124
+ req.add_field(Datadog::Ext::DistributedTracing::HTTP_HEADER_TRACE_ID, span.trace_id)
125
+ req.add_field(Datadog::Ext::DistributedTracing::HTTP_HEADER_PARENT_ID, span.span_id)
126
+ end
127
+ rescue StandardError => e
128
+ Datadog::Tracer.log.error("error preparing span for http request: #{e}")
129
+ ensure
130
+ response = request_without_datadog(req, body, &block)
131
+ end
105
132
  span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, response.code)
106
133
  if req.respond_to?(:uri) && req.uri
107
134
  span.set_tag(Datadog::Ext::NET::TARGET_HOST, req.uri.host)
@@ -1,11 +1,21 @@
1
1
  require 'ddtrace/ext/app_types'
2
2
  require 'ddtrace/ext/http'
3
+ require 'ddtrace/distributed'
3
4
 
4
5
  module Datadog
5
6
  module Contrib
6
7
  # Rack module includes middlewares that are required to trace any framework
7
8
  # and application built on top of Rack.
8
9
  module Rack
10
+ # RACK headers to test when doing distributed tracing.
11
+ # They are slightly different from real headers as Rack uppercases everything
12
+
13
+ # Header used to transmit the trace ID.
14
+ HTTP_HEADER_TRACE_ID = 'HTTP_X_DATADOG_TRACE_ID'.freeze
15
+
16
+ # Header used to transmit the parent ID.
17
+ HTTP_HEADER_PARENT_ID = 'HTTP_X_DATADOG_PARENT_ID'.freeze
18
+
9
19
  # TraceMiddleware ensures that the Rack Request is properly traced
10
20
  # from the beginning to the end. The middleware adds the request span
11
21
  # in the Rack environment so that it can be retrieved by the underlying
@@ -14,13 +24,15 @@ module Datadog
14
24
  class TraceMiddleware
15
25
  DEFAULT_CONFIG = {
16
26
  tracer: Datadog.tracer,
17
- default_service: 'rack'
27
+ default_service: 'rack',
28
+ distributed_tracing_enabled: false
18
29
  }.freeze
19
30
 
20
31
  def initialize(app, options = {})
21
32
  # update options with our configuration, unless it's already available
22
- options[:tracer] ||= DEFAULT_CONFIG[:tracer]
23
- options[:default_service] ||= DEFAULT_CONFIG[:default_service]
33
+ [:tracer, :default_service, :distributed_tracing_enabled].each do |k|
34
+ options[k] ||= DEFAULT_CONFIG[k]
35
+ end
24
36
 
25
37
  @app = app
26
38
  @options = options
@@ -33,6 +45,7 @@ module Datadog
33
45
  # retrieve the current tracer and service
34
46
  @tracer = @options.fetch(:tracer)
35
47
  @service = @options.fetch(:default_service)
48
+ @distributed_tracing_enabled = @options.fetch(:distributed_tracing_enabled)
36
49
 
37
50
  # configure the Rack service
38
51
  @tracer.set_service_info(
@@ -42,23 +55,45 @@ module Datadog
42
55
  )
43
56
  end
44
57
 
58
+ # rubocop:disable Metrics/MethodLength
45
59
  def call(env)
46
60
  # configure the Rack middleware once
47
61
  configure()
48
62
 
49
- # start a new request span and attach it to the current Rack environment;
50
- # we must ensure that the span `resource` is set later
51
- request_span = @tracer.trace(
52
- 'rack.request',
63
+ trace_options = {
53
64
  service: @service,
54
65
  resource: nil,
55
66
  span_type: Datadog::Ext::HTTP::TYPE
56
- )
67
+ }
68
+
69
+ # start a new request span and attach it to the current Rack environment;
70
+ # we must ensure that the span `resource` is set later
71
+ request_span = @tracer.trace('rack.request', trace_options)
72
+
73
+ if @distributed_tracing_enabled
74
+ # Merge distributed trace ids if present
75
+ #
76
+ # Use integer values for tests, as it will catch both
77
+ # a non-existing header or a badly formed one.
78
+ trace_id, parent_id = Datadog::Distributed.parse_trace_headers(
79
+ env[Datadog::Contrib::Rack::HTTP_HEADER_TRACE_ID],
80
+ env[Datadog::Contrib::Rack::HTTP_HEADER_PARENT_ID]
81
+ )
82
+ request_span.trace_id = trace_id unless trace_id.nil?
83
+ request_span.parent_id = parent_id unless parent_id.nil?
84
+ end
85
+
57
86
  env[:datadog_rack_request_span] = request_span
58
87
 
59
88
  # call the rest of the stack
60
89
  status, headers, response = @app.call(env)
61
- rescue StandardError => e
90
+ # rubocop:disable Lint/RescueException
91
+ # Here we really want to catch *any* exception, not only StandardError,
92
+ # as we really have no clue of what is in the block,
93
+ # and it is user code which should be executed no matter what.
94
+ # It's not a problem since we re-raise it afterwards so for example a
95
+ # SignalException::Interrupt would still bubble up.
96
+ rescue Exception => e
62
97
  # catch exceptions that may be raised in the middleware chain
63
98
  # Note: if a middleware catches an Exception without re raising,
64
99
  # the Exception cannot be recorded here
@@ -79,9 +114,15 @@ module Datadog
79
114
  # be set in another level but if they're missing, reasonable defaults
80
115
  # are used.
81
116
  request_span.resource = "#{env['REQUEST_METHOD']} #{status}".strip unless request_span.resource
82
- request_span.set_tag('http.method', env['REQUEST_METHOD']) if request_span.get_tag('http.method').nil?
83
- request_span.set_tag('http.url', url) if request_span.get_tag('http.url').nil?
84
- request_span.set_tag('http.status_code', status) if request_span.get_tag('http.status_code').nil? && status
117
+ if request_span.get_tag(Datadog::Ext::HTTP::METHOD).nil?
118
+ request_span.set_tag(Datadog::Ext::HTTP::METHOD, env['REQUEST_METHOD'])
119
+ end
120
+ if request_span.get_tag(Datadog::Ext::HTTP::URL).nil?
121
+ request_span.set_tag(Datadog::Ext::HTTP::URL, url)
122
+ end
123
+ if request_span.get_tag(Datadog::Ext::HTTP::STATUS_CODE).nil? && status
124
+ request_span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, status)
125
+ end
85
126
 
86
127
  # detect if the status code is a 5xx and flag the request span as an error
87
128
  # unless it has been already set by the underlying framework
@@ -33,6 +33,7 @@ module Datadog
33
33
  Datadog::Tracer.log.error(e.message)
34
34
  end
35
35
 
36
+ # rubocop:disable Metrics/MethodLength
36
37
  def self.process_action(_name, start, finish, _id, payload)
37
38
  return unless Thread.current[KEY]
38
39
  Thread.current[KEY] = false
@@ -63,14 +64,22 @@ module Datadog
63
64
  end
64
65
  else
65
66
  error = payload[:exception]
66
- span.status = 1
67
- span.set_tag(Datadog::Ext::Errors::TYPE, error[0])
68
- span.set_tag(Datadog::Ext::Errors::MSG, error[1])
69
- span.set_tag(Datadog::Ext::Errors::STACK, caller().join("\n"))
67
+ if defined?(::ActionDispatch::ExceptionWrapper)
68
+ status = ::ActionDispatch::ExceptionWrapper.status_code_for_exception(error[0])
69
+ status = status ? status.to_s : '?'
70
+ else
71
+ status = '500'
72
+ end
73
+ if status.starts_with?('5')
74
+ span.status = 1
75
+ span.set_tag(Datadog::Ext::Errors::TYPE, error[0])
76
+ span.set_tag(Datadog::Ext::Errors::MSG, error[1])
77
+ span.set_tag(Datadog::Ext::Errors::STACK, caller().join("\n"))
78
+ end
70
79
  end
71
80
  ensure
72
81
  span.start_time = start
73
- span.finish_at(finish)
82
+ span.finish(finish)
74
83
  end
75
84
  rescue StandardError => e
76
85
  Datadog::Tracer.log.error(e.message)
@@ -7,7 +7,7 @@ module Datadog
7
7
  module ActionView
8
8
  def self.instrument
9
9
  # patch Rails core components
10
- Datadog::RailsPatcher.patch_renderer()
10
+ Datadog::RailsRendererPatcher.patch_renderer()
11
11
 
12
12
  # subscribe when the template rendering starts
13
13
  ::ActiveSupport::Notifications.subscribe('start_render_template.action_view') do |*args|
@@ -83,7 +83,7 @@ module Datadog
83
83
  end
84
84
  ensure
85
85
  span.start_time = start
86
- span.finish_at(finish)
86
+ span.finish(finish)
87
87
  end
88
88
  rescue StandardError => e
89
89
  Datadog::Tracer.log.error(e.message)
@@ -111,7 +111,7 @@ module Datadog
111
111
  end
112
112
  ensure
113
113
  span.start_time = start
114
- span.finish_at(finish)
114
+ span.finish(finish)
115
115
  end
116
116
  rescue StandardError => e
117
117
  Datadog::Tracer.log.error(e.message)