ddtrace 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.env +11 -0
  3. data/.gitignore +56 -0
  4. data/.rubocop.yml +43 -0
  5. data/Appraisals +65 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +24 -0
  8. data/README.md +119 -0
  9. data/Rakefile +103 -0
  10. data/circle.yml +68 -0
  11. data/ddtrace.gemspec +41 -0
  12. data/docker-compose.yml +33 -0
  13. data/docs/GettingStarted +352 -0
  14. data/gemfiles/contrib.gemfile +9 -0
  15. data/gemfiles/rails3_mysql2.gemfile +11 -0
  16. data/gemfiles/rails3_postgres.gemfile +10 -0
  17. data/gemfiles/rails3_postgres_redis.gemfile +11 -0
  18. data/gemfiles/rails4_mysql2.gemfile +9 -0
  19. data/gemfiles/rails4_postgres.gemfile +9 -0
  20. data/gemfiles/rails4_postgres_redis.gemfile +10 -0
  21. data/gemfiles/rails5_mysql2.gemfile +8 -0
  22. data/gemfiles/rails5_postgres.gemfile +8 -0
  23. data/gemfiles/rails5_postgres_redis.gemfile +9 -0
  24. data/lib/ddtrace.rb +63 -0
  25. data/lib/ddtrace/buffer.rb +77 -0
  26. data/lib/ddtrace/contrib/elasticsearch/core.rb +56 -0
  27. data/lib/ddtrace/contrib/elasticsearch/patcher.rb +35 -0
  28. data/lib/ddtrace/contrib/elasticsearch/quantize.rb +22 -0
  29. data/lib/ddtrace/contrib/rails/action_controller.rb +75 -0
  30. data/lib/ddtrace/contrib/rails/action_view.rb +121 -0
  31. data/lib/ddtrace/contrib/rails/active_record.rb +44 -0
  32. data/lib/ddtrace/contrib/rails/active_support.rb +115 -0
  33. data/lib/ddtrace/contrib/rails/core_extensions.rb +89 -0
  34. data/lib/ddtrace/contrib/rails/framework.rb +107 -0
  35. data/lib/ddtrace/contrib/rails/utils.rb +42 -0
  36. data/lib/ddtrace/contrib/redis/core.rb +72 -0
  37. data/lib/ddtrace/contrib/redis/patcher.rb +36 -0
  38. data/lib/ddtrace/contrib/redis/quantize.rb +30 -0
  39. data/lib/ddtrace/contrib/redis/tags.rb +19 -0
  40. data/lib/ddtrace/encoding.rb +65 -0
  41. data/lib/ddtrace/ext/app_types.rb +9 -0
  42. data/lib/ddtrace/ext/cache.rb +7 -0
  43. data/lib/ddtrace/ext/errors.rb +9 -0
  44. data/lib/ddtrace/ext/http.rb +11 -0
  45. data/lib/ddtrace/ext/net.rb +8 -0
  46. data/lib/ddtrace/ext/redis.rb +16 -0
  47. data/lib/ddtrace/ext/sql.rb +8 -0
  48. data/lib/ddtrace/monkey.rb +60 -0
  49. data/lib/ddtrace/pin.rb +62 -0
  50. data/lib/ddtrace/span.rb +163 -0
  51. data/lib/ddtrace/tracer.rb +180 -0
  52. data/lib/ddtrace/transport.rb +149 -0
  53. data/lib/ddtrace/utils.rb +9 -0
  54. data/lib/ddtrace/version.rb +9 -0
  55. data/lib/ddtrace/workers.rb +109 -0
  56. data/lib/ddtrace/writer.rb +119 -0
  57. metadata +187 -0
@@ -0,0 +1,180 @@
1
+ require 'pp'
2
+ require 'thread'
3
+ require 'logger'
4
+
5
+ require 'ddtrace/span'
6
+ require 'ddtrace/buffer'
7
+ require 'ddtrace/writer'
8
+
9
+ # \Datadog global namespace that includes all tracing functionality for Tracer and Span classes.
10
+ module Datadog
11
+ # A \Tracer keeps track of the time spent by an application processing a single operation. For
12
+ # example, a trace can be used to track the entire time spent processing a complicated web request.
13
+ # Even though the request may require multiple resources and machines to handle the request, all
14
+ # of these function calls and sub-requests would be encapsulated within a single trace.
15
+ class Tracer
16
+ attr_reader :writer, :services
17
+ attr_accessor :enabled
18
+
19
+ # Global, memoized, lazy initialized instance of a logger that is used within the the Datadog
20
+ # namespace. This logger outputs to +STDOUT+ by default, and is considered thread-safe.
21
+ def self.log
22
+ unless defined? @logger
23
+ @logger = Logger.new(STDOUT)
24
+ @logger.level = Logger::INFO
25
+ end
26
+ @logger
27
+ end
28
+
29
+ # Activate the debug mode providing more information related to tracer usage
30
+ def self.debug_logging=(value)
31
+ log.level = value ? Logger::DEBUG : Logger::INFO
32
+ end
33
+
34
+ # Return if the debug mode is activated or not
35
+ def self.debug_logging
36
+ log.level == Logger::DEBUG
37
+ end
38
+
39
+ # Initialize a new \Tracer used to create, sample and submit spans that measure the
40
+ # time of sections of code. Available +options+ are:
41
+ #
42
+ # * +enabled+: set if the tracer submits or not spans to the local agent. It's enabled
43
+ # by default.
44
+ def initialize(options = {})
45
+ @enabled = options.fetch(:enabled, true)
46
+ @writer = options.fetch(:writer, Datadog::Writer.new)
47
+ @buffer = Datadog::SpanBuffer.new()
48
+
49
+ @mutex = Mutex.new
50
+ @spans = []
51
+ @services = {}
52
+ end
53
+
54
+ # Updates the current \Tracer instance, so that the tracer can be configured after the
55
+ # initialization. Available +options+ are:
56
+ #
57
+ # * +enabled+: set if the tracer submits or not spans to the trace agent
58
+ # * +hostname+: change the location of the trace agent
59
+ # * +port+: change the port of the trace agent
60
+ #
61
+ # For instance, if the trace agent runs in a different location, just:
62
+ #
63
+ # tracer.configure(hostname: 'agent.service.consul', port: '8777')
64
+ #
65
+ def configure(options = {})
66
+ enabled = options.fetch(:enabled, nil)
67
+ hostname = options.fetch(:hostname, nil)
68
+ port = options.fetch(:port, nil)
69
+
70
+ @enabled = enabled unless enabled.nil?
71
+ @writer.transport.hostname = hostname unless hostname.nil?
72
+ @writer.transport.port = port unless port.nil?
73
+ end
74
+
75
+ # Set the information about the given service. A valid example is:
76
+ #
77
+ # tracer.set_service_info('web-application', 'rails', 'web')
78
+ def set_service_info(service, app, app_type)
79
+ @services[service] = {
80
+ 'app' => app,
81
+ 'app_type' => app_type
82
+ }
83
+
84
+ return unless Datadog::Tracer.debug_logging
85
+ Datadog::Tracer.log.debug("set_service_info: service: #{service} app: #{app} type: #{app_type}")
86
+ end
87
+
88
+ # Return a +span+ that will trace an operation called +name+. You could trace your code
89
+ # using a <tt>do-block</tt> like:
90
+ #
91
+ # tracer.trace('web.request') do |span|
92
+ # span.service = 'my-web-site'
93
+ # span.resource = '/'
94
+ # span.set_tag('http.method', request.request_method)
95
+ # do_something()
96
+ # end
97
+ #
98
+ # The <tt>tracer.trace()</tt> method can also be used without a block in this way:
99
+ #
100
+ # span = tracer.trace('web.request', service: 'my-web-site')
101
+ # do_something()
102
+ # span.finish()
103
+ #
104
+ # Remember that in this case, calling <tt>span.finish()</tt> is mandatory.
105
+ #
106
+ # When a Trace is started, <tt>trace()</tt> will store the created span; subsequent spans will
107
+ # become it's children and will inherit some properties:
108
+ #
109
+ # parent = tracer.trace('parent') # has no parent span
110
+ # child = tracer.trace('child') # is a child of 'parent'
111
+ # child.finish()
112
+ # parent.finish()
113
+ # parent2 = tracer.trace('parent2') # has no parent span
114
+ # parent2.finish()
115
+ #
116
+ def trace(name, options = {})
117
+ span = Span.new(self, name, options)
118
+
119
+ # set up inheritance
120
+ parent = @buffer.get()
121
+ span.set_parent(parent)
122
+ @buffer.set(span)
123
+
124
+ # call the finish only if a block is given; this ensures
125
+ # that a call to tracer.trace() without a block, returns
126
+ # a span that should be manually finished.
127
+ if block_given?
128
+ begin
129
+ yield(span)
130
+ rescue StandardError => e
131
+ span.set_error(e)
132
+ raise
133
+ ensure
134
+ span.finish()
135
+ end
136
+ else
137
+ span
138
+ end
139
+ end
140
+
141
+ # Record the given finished span in the +spans+ list. When a +span+ is recorded, it will be sent
142
+ # to the Datadog trace agent as soon as the trace is finished.
143
+ def record(span)
144
+ spans = []
145
+ @mutex.synchronize do
146
+ @spans << span
147
+ parent = span.parent
148
+ # Bubble up until we find a non-finished parent. This is necessary for
149
+ # the case when the parent finished after its parent.
150
+ parent = parent.parent while !parent.nil? && parent.finished?
151
+ @buffer.set(parent)
152
+
153
+ return unless parent.nil?
154
+ spans = @spans
155
+ @spans = []
156
+ end
157
+
158
+ return if spans.empty?
159
+ write(spans)
160
+ end
161
+
162
+ # Return the current active span or +nil+.
163
+ def active_span
164
+ @buffer.get()
165
+ end
166
+
167
+ def write(spans)
168
+ return if @writer.nil? || !@enabled
169
+
170
+ if Datadog::Tracer.debug_logging
171
+ Datadog::Tracer.log.debug("Writing #{spans.length} spans (enabled: #{@enabled})")
172
+ PP.pp(spans)
173
+ end
174
+
175
+ @writer.write(spans, @services)
176
+ end
177
+
178
+ private :write
179
+ end
180
+ end
@@ -0,0 +1,149 @@
1
+ require 'thread'
2
+ require 'net/http'
3
+
4
+ require 'ddtrace/encoding'
5
+
6
+ module Datadog
7
+ # Transport class that handles the spans delivery to the
8
+ # local trace-agent. The class wraps a Net:HTTP instance
9
+ # so that the Transport is thread-safe.
10
+ class HTTPTransport
11
+ attr_accessor :hostname, :port
12
+
13
+ # seconds before the transport timeout
14
+ TIMEOUT = 1
15
+
16
+ def initialize(hostname, port, options = {})
17
+ @hostname = hostname
18
+ @port = port
19
+ @traces_endpoint = '/v0.3/traces'.freeze
20
+ @services_endpoint = '/v0.3/services'.freeze
21
+ @compatibility_mode = false
22
+ @encoder = options.fetch(:encoder, Datadog::Encoding::MsgpackEncoder.new())
23
+
24
+ # overwrite the Content-type with the one chosen in the Encoder
25
+ @headers = options.fetch(:headers, {})
26
+ @headers['Content-Type'] = @encoder.content_type
27
+
28
+ # stats
29
+ @mutex = Mutex.new
30
+ @count_success = 0
31
+ @count_client_error = 0
32
+ @count_server_error = 0
33
+ @count_internal_error = 0
34
+ end
35
+
36
+ # route the send to the right endpoint
37
+ def send(endpoint, data)
38
+ case endpoint
39
+ when :services
40
+ payload = @encoder.encode_services(data)
41
+ status_code = post(@services_endpoint, payload)
42
+ when :traces
43
+ payload = @encoder.encode_traces(data)
44
+ status_code = post(@traces_endpoint, payload)
45
+ else
46
+ Datadog::Tracer.log.error("Unsupported endpoint: #{endpoint}")
47
+ return nil
48
+ end
49
+
50
+ return status_code unless downgrade?(status_code) && !@compatibility_mode
51
+
52
+ # the API endpoint is not available so we should downgrade the connection and re-try the call
53
+ downgrade!
54
+ send(endpoint, data)
55
+ end
56
+
57
+ # send data to the trace-agent; the method is thread-safe
58
+ def post(url, data)
59
+ Datadog::Tracer.log.debug("Sending data from process: #{Process.pid}")
60
+ request = Net::HTTP::Post.new(url, @headers)
61
+ request.body = data
62
+
63
+ response = Net::HTTP.start(@hostname, @port, read_timeout: TIMEOUT) { |http| http.request(request) }
64
+ handle_response(response)
65
+ rescue StandardError => e
66
+ Datadog::Tracer.log.error(e.message)
67
+ 500
68
+ end
69
+
70
+ # Downgrade the connection to a compatibility version of the HTTPTransport;
71
+ # this method should target a stable API that works whatever is the agent
72
+ # or the tracing client versions.
73
+ def downgrade!
74
+ @compatibility_mode = true
75
+ @traces_endpoint = '/v0.2/traces'.freeze
76
+ @services_endpoint = '/v0.2/services'.freeze
77
+ @encoder = Datadog::Encoding::JSONEncoder.new()
78
+ @headers['Content-Type'] = @encoder.content_type
79
+ end
80
+
81
+ def informational?(code)
82
+ code.between?(100, 199)
83
+ end
84
+
85
+ def success?(code)
86
+ code.between?(200, 299)
87
+ end
88
+
89
+ def redirect?(code)
90
+ code.between?(300, 399)
91
+ end
92
+
93
+ def client_error?(code)
94
+ code.between?(400, 499)
95
+ end
96
+
97
+ def server_error?(code)
98
+ code.between?(500, 599)
99
+ end
100
+
101
+ # receiving a 404 means that we're targeting an endpoint that is not available
102
+ # in the trace agent. Usually this means that we've an up-to-date tracing client,
103
+ # while running an obsolete agent.
104
+ # receiving a 415 means that we're using an unsupported content-type with an existing
105
+ # endpoint. Usually this means that we're using a newer encoder with a previous
106
+ # endpoint. In both cases, we're going to downgrade the transporter encoder so that
107
+ # it will target a stable API.
108
+ def downgrade?(code)
109
+ code == 404 || code == 415
110
+ end
111
+
112
+ # handles the server response; here you can log the trace-agent response
113
+ # or do something more complex to recover from a possible error. This
114
+ # function is handled within the HTTP mutex.synchronize so it's thread-safe.
115
+ def handle_response(response)
116
+ status_code = response.code.to_i
117
+
118
+ if success?(status_code)
119
+ Datadog::Tracer.log.debug('Payload correctly sent to the trace agent.')
120
+ @mutex.synchronize { @count_success += 1 }
121
+ elsif downgrade?(status_code)
122
+ Datadog::Tracer.log.debug("calling the endpoint but received #{status_code}; downgrading the API")
123
+ elsif client_error?(status_code)
124
+ Datadog::Tracer.log.error("Client error: #{response.message}")
125
+ @mutex.synchronize { @count_client_error += 1 }
126
+ elsif server_error?(status_code)
127
+ Datadog::Tracer.log.error("Server error: #{response.message}")
128
+ @mutex.synchronize { @count_server_error += 1 }
129
+ end
130
+
131
+ status_code
132
+ rescue StandardError => e
133
+ Datadog::Tracer.log.error(e.message)
134
+ @mutex.synchronize { @count_internal_error += 1 }
135
+ 500
136
+ end
137
+
138
+ def stats
139
+ @mutex.synchronize do
140
+ {
141
+ success: @count_success,
142
+ client_error: @count_client_error,
143
+ server_error: @count_server_error,
144
+ internal_error: @count_internal_error
145
+ }
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,9 @@
1
+ module Datadog
2
+ # TODO[manu]: write docs
3
+ module Utils
4
+ # Return a span id
5
+ def self.next_id
6
+ rand(Datadog::Span::MAX_ID)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Datadog
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 3
5
+ PATCH = 1
6
+
7
+ STRING = [MAJOR, MINOR, PATCH].compact.join('.')
8
+ end
9
+ end
@@ -0,0 +1,109 @@
1
+ require 'time'
2
+
3
+ require 'ddtrace/buffer'
4
+
5
+ module Datadog
6
+ module Workers
7
+ # Asynchronous worker that executes a +Send()+ operation after given
8
+ # seconds. Under the hood, it uses +Concurrent::TimerTask+ so that the thread
9
+ # will perform a task at regular intervals. The thread can be stopped
10
+ # with the +stop()+ method and can start with the +start()+ method.
11
+ class AsyncTransport
12
+ def initialize(span_interval, service_interval, transport, buff_size, trace_task, service_task)
13
+ @trace_task = trace_task
14
+ @service_task = service_task
15
+ @span_interval = span_interval
16
+ @service_interval = service_interval
17
+ @trace_buffer = TraceBuffer.new(buff_size)
18
+ @service_buffer = TraceBuffer.new(buff_size)
19
+ @transport = transport
20
+
21
+ @worker = nil
22
+ @run = false
23
+ end
24
+
25
+ # Callback function that process traces and executes the +send_traces()+ method.
26
+ def callback_traces
27
+ return if @trace_buffer.empty?
28
+
29
+ begin
30
+ traces = @trace_buffer.pop()
31
+ @trace_task.call(traces, @transport)
32
+ rescue StandardError => e
33
+ # ensures that the thread will not die because of an exception.
34
+ # TODO[manu]: findout the reason and reschedule the send if it's not
35
+ # a fatal exception
36
+ Datadog::Tracer.log.error("Error during traces flush: dropped #{items.length} items. Cause: #{e}")
37
+ end
38
+ end
39
+
40
+ # Callback function that process traces and executes the +send_services()+ method.
41
+ def callback_services
42
+ return if @service_buffer.empty?
43
+
44
+ begin
45
+ services = @service_buffer.pop()
46
+ # pick up the latest services hash (this is a FIFO list)
47
+ # that is different from what we sent before.
48
+ different = services.inject(false) { |acc, elem| elem != @last_flushed_services ? elem : acc }
49
+ if different
50
+ if @service_task.call(different, @transport)
51
+ @last_flushed_services = different.clone
52
+ end
53
+ else
54
+ Datadog::Tracer.log.debug('No new different services, skipping flush.')
55
+ end
56
+ rescue StandardError => e
57
+ # ensures that the thread will not die because of an exception.
58
+ # TODO[manu]: findout the reason and reschedule the send if it's not
59
+ # a fatal exception
60
+ Datadog::Tracer.log.error("Error during services flush: dropped #{items.length} items. Cause: #{e}")
61
+ end
62
+ end
63
+
64
+ # Start the timer execution.
65
+ def start
66
+ return if @run
67
+ @run = true
68
+ @worker = Thread.new() do
69
+ Datadog::Tracer.log.debug("Starting thread in the process: #{Process.pid}")
70
+ @last_flushed_services = nil
71
+ next_send_services = Time.now
72
+
73
+ # this loop assumes spans are flushed more often than services
74
+ while @run
75
+ callback_traces
76
+ if Time.now >= next_send_services
77
+ next_send_services = Time.now + @service_interval
78
+ callback_services
79
+ end
80
+ sleep(@span_interval)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Stop the timer execution. Tasks already in the queue will be executed.
86
+ def stop
87
+ @run = false
88
+ end
89
+
90
+ # Block until executor shutdown is complete or until timeout seconds have passed.
91
+ def join
92
+ @worker.join(10)
93
+ end
94
+
95
+ # Enqueue an item in the trace internal buffer. This operation is thread-safe
96
+ # because uses the +TraceBuffer+ data structure.
97
+ def enqueue_trace(trace)
98
+ @trace_buffer.push(trace)
99
+ end
100
+
101
+ # Enqueue an item in the service internal buffer. This operation is thread-safe
102
+ # because uses the +TraceBuffer+ data structure.
103
+ def enqueue_service(service)
104
+ return if service == {} # no use to send this, not worth it
105
+ @service_buffer.push(service)
106
+ end
107
+ end
108
+ end
109
+ end