ddtrace 0.7.2 → 0.8.0

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