atatus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile +57 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-THIRD-PARTY +205 -0
  7. data/README.md +13 -0
  8. data/Rakefile +19 -0
  9. data/atatus.gemspec +36 -0
  10. data/atatus.yml +2 -0
  11. data/bench/.gitignore +2 -0
  12. data/bench/app.rb +53 -0
  13. data/bench/benchmark.rb +36 -0
  14. data/bench/report.rb +55 -0
  15. data/bench/rubyprof.rb +39 -0
  16. data/bench/stackprof.rb +23 -0
  17. data/bin/build_docs +5 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/bin/with_framework +7 -0
  21. data/lib/atatus.rb +325 -0
  22. data/lib/atatus/agent.rb +260 -0
  23. data/lib/atatus/central_config.rb +141 -0
  24. data/lib/atatus/central_config/cache_control.rb +34 -0
  25. data/lib/atatus/collector/base.rb +329 -0
  26. data/lib/atatus/collector/builder.rb +317 -0
  27. data/lib/atatus/collector/transport.rb +72 -0
  28. data/lib/atatus/config.rb +248 -0
  29. data/lib/atatus/config/bytes.rb +25 -0
  30. data/lib/atatus/config/duration.rb +23 -0
  31. data/lib/atatus/config/options.rb +134 -0
  32. data/lib/atatus/config/regexp_list.rb +13 -0
  33. data/lib/atatus/context.rb +33 -0
  34. data/lib/atatus/context/request.rb +11 -0
  35. data/lib/atatus/context/request/socket.rb +19 -0
  36. data/lib/atatus/context/request/url.rb +42 -0
  37. data/lib/atatus/context/response.rb +22 -0
  38. data/lib/atatus/context/user.rb +42 -0
  39. data/lib/atatus/context_builder.rb +97 -0
  40. data/lib/atatus/deprecations.rb +22 -0
  41. data/lib/atatus/error.rb +22 -0
  42. data/lib/atatus/error/exception.rb +46 -0
  43. data/lib/atatus/error/log.rb +24 -0
  44. data/lib/atatus/error_builder.rb +76 -0
  45. data/lib/atatus/instrumenter.rb +224 -0
  46. data/lib/atatus/internal_error.rb +6 -0
  47. data/lib/atatus/logging.rb +55 -0
  48. data/lib/atatus/metadata.rb +19 -0
  49. data/lib/atatus/metadata/process_info.rb +18 -0
  50. data/lib/atatus/metadata/service_info.rb +61 -0
  51. data/lib/atatus/metadata/system_info.rb +35 -0
  52. data/lib/atatus/metadata/system_info/container_info.rb +121 -0
  53. data/lib/atatus/metadata/system_info/hw_info.rb +118 -0
  54. data/lib/atatus/metadata/system_info/os_info.rb +31 -0
  55. data/lib/atatus/metrics.rb +98 -0
  56. data/lib/atatus/metrics/cpu_mem.rb +240 -0
  57. data/lib/atatus/metrics/vm.rb +60 -0
  58. data/lib/atatus/metricset.rb +19 -0
  59. data/lib/atatus/middleware.rb +76 -0
  60. data/lib/atatus/naively_hashable.rb +21 -0
  61. data/lib/atatus/normalizers.rb +68 -0
  62. data/lib/atatus/normalizers/action_controller.rb +27 -0
  63. data/lib/atatus/normalizers/action_mailer.rb +26 -0
  64. data/lib/atatus/normalizers/action_view.rb +77 -0
  65. data/lib/atatus/normalizers/active_record.rb +45 -0
  66. data/lib/atatus/opentracing.rb +346 -0
  67. data/lib/atatus/rails.rb +61 -0
  68. data/lib/atatus/railtie.rb +30 -0
  69. data/lib/atatus/span.rb +125 -0
  70. data/lib/atatus/span/context.rb +40 -0
  71. data/lib/atatus/span_helpers.rb +44 -0
  72. data/lib/atatus/spies.rb +86 -0
  73. data/lib/atatus/spies/action_dispatch.rb +28 -0
  74. data/lib/atatus/spies/delayed_job.rb +68 -0
  75. data/lib/atatus/spies/elasticsearch.rb +36 -0
  76. data/lib/atatus/spies/faraday.rb +70 -0
  77. data/lib/atatus/spies/http.rb +44 -0
  78. data/lib/atatus/spies/json.rb +22 -0
  79. data/lib/atatus/spies/mongo.rb +87 -0
  80. data/lib/atatus/spies/net_http.rb +70 -0
  81. data/lib/atatus/spies/rake.rb +45 -0
  82. data/lib/atatus/spies/redis.rb +27 -0
  83. data/lib/atatus/spies/sequel.rb +47 -0
  84. data/lib/atatus/spies/sidekiq.rb +89 -0
  85. data/lib/atatus/spies/sinatra.rb +41 -0
  86. data/lib/atatus/spies/tilt.rb +27 -0
  87. data/lib/atatus/sql_summarizer.rb +35 -0
  88. data/lib/atatus/stacktrace.rb +16 -0
  89. data/lib/atatus/stacktrace/frame.rb +52 -0
  90. data/lib/atatus/stacktrace_builder.rb +104 -0
  91. data/lib/atatus/subscriber.rb +77 -0
  92. data/lib/atatus/trace_context.rb +85 -0
  93. data/lib/atatus/transaction.rb +100 -0
  94. data/lib/atatus/transport/base.rb +174 -0
  95. data/lib/atatus/transport/connection.rb +156 -0
  96. data/lib/atatus/transport/connection/http.rb +116 -0
  97. data/lib/atatus/transport/connection/proxy_pipe.rb +75 -0
  98. data/lib/atatus/transport/filters.rb +43 -0
  99. data/lib/atatus/transport/filters/secrets_filter.rb +74 -0
  100. data/lib/atatus/transport/serializers.rb +93 -0
  101. data/lib/atatus/transport/serializers/context_serializer.rb +85 -0
  102. data/lib/atatus/transport/serializers/error_serializer.rb +77 -0
  103. data/lib/atatus/transport/serializers/metadata_serializer.rb +70 -0
  104. data/lib/atatus/transport/serializers/metricset_serializer.rb +28 -0
  105. data/lib/atatus/transport/serializers/span_serializer.rb +80 -0
  106. data/lib/atatus/transport/serializers/transaction_serializer.rb +37 -0
  107. data/lib/atatus/transport/worker.rb +73 -0
  108. data/lib/atatus/util.rb +42 -0
  109. data/lib/atatus/util/inflector.rb +93 -0
  110. data/lib/atatus/util/lru_cache.rb +48 -0
  111. data/lib/atatus/util/prefixed_logger.rb +18 -0
  112. data/lib/atatus/util/throttle.rb +35 -0
  113. data/lib/atatus/version.rb +5 -0
  114. data/vendor/.gitkeep +0 -0
  115. metadata +190 -0
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ # @api private
5
+ class TraceContext
6
+ class InvalidTraceparentHeader < StandardError; end
7
+
8
+ VERSION = '00'
9
+ HEX_REGEX = /[^[:xdigit:]]/.freeze
10
+
11
+ TRACE_ID_LENGTH = 16
12
+ ID_LENGTH = 8
13
+
14
+ def initialize(
15
+ version: VERSION,
16
+ trace_id: nil,
17
+ span_id: nil,
18
+ id: nil,
19
+ recorded: true
20
+ )
21
+ @version = version
22
+ @trace_id = trace_id || hex(TRACE_ID_LENGTH)
23
+ # TODO: rename to parent_id with next major version bump
24
+ @parent_id = span_id
25
+ @id = id || hex(ID_LENGTH)
26
+ @recorded = recorded
27
+ end
28
+
29
+ attr_accessor :version, :id, :trace_id, :parent_id, :recorded
30
+
31
+ alias :recorded? :recorded
32
+
33
+ # rubocop:disable Metrics/AbcSize
34
+ def self.parse(header)
35
+ raise InvalidTraceparentHeader unless header.length == 55
36
+ raise InvalidTraceparentHeader unless header[0..1] == VERSION
37
+
38
+ new.tap do |t|
39
+ t.version, t.trace_id, t.parent_id, t.flags =
40
+ header.split('-').tap do |values|
41
+ values[-1] = Util.hex_to_bits(values[-1])
42
+ end
43
+
44
+ raise InvalidTraceparentHeader if HEX_REGEX =~ t.trace_id
45
+ raise InvalidTraceparentHeader if HEX_REGEX =~ t.parent_id
46
+ end
47
+ end
48
+ # rubocop:enable Metrics/AbcSize
49
+
50
+ def flags=(flags)
51
+ @flags = flags
52
+
53
+ self.recorded = flags[7] == '1'
54
+ end
55
+
56
+ def flags
57
+ format('0000000%d', recorded? ? 1 : 0)
58
+ end
59
+
60
+ def hex_flags
61
+ format('%02x', flags.to_i(2))
62
+ end
63
+
64
+ def ensure_parent_id
65
+ @parent_id ||= hex(ID_LENGTH)
66
+ end
67
+
68
+ def child
69
+ dup.tap do |tc|
70
+ tc.parent_id = tc.id
71
+ tc.id = hex(ID_LENGTH)
72
+ end
73
+ end
74
+
75
+ def to_header
76
+ format('%s-%s-%s-%s', version, trace_id, id, hex_flags)
77
+ end
78
+
79
+ private
80
+
81
+ def hex(len)
82
+ SecureRandom.hex(len)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'forwardable'
5
+
6
+ module Atatus
7
+ # @api private
8
+ class Transaction
9
+ extend Forwardable
10
+
11
+ def_delegators :@trace_context,
12
+ :trace_id, :parent_id, :id, :ensure_parent_id
13
+
14
+ DEFAULT_TYPE = 'custom'
15
+
16
+ # rubocop:disable Metrics/ParameterLists
17
+ def initialize(
18
+ name = nil,
19
+ type = nil,
20
+ sampled: true,
21
+ context: nil,
22
+ labels: nil,
23
+ trace_context: nil
24
+ )
25
+ @name = name
26
+ @type = type || DEFAULT_TYPE
27
+
28
+ @sampled = sampled
29
+
30
+ @context = context || Context.new # TODO: Lazy generate this?
31
+ Util.reverse_merge!(@context.labels, labels) if labels
32
+
33
+ @trace_context = trace_context || TraceContext.new(recorded: sampled)
34
+
35
+ @started_spans = 0
36
+ @dropped_spans = 0
37
+
38
+ @notifications = [] # for AS::Notifications
39
+ end
40
+ # rubocop:enable Metrics/ParameterLists
41
+
42
+ attr_accessor :name, :type, :result, :spans
43
+
44
+ attr_reader :context, :duration, :started_spans, :dropped_spans,
45
+ :timestamp, :trace_context, :notifications
46
+
47
+ def sampled?
48
+ @sampled
49
+ end
50
+
51
+ def stopped?
52
+ !!duration
53
+ end
54
+
55
+ # life cycle
56
+
57
+ def start(clock_start = Util.monotonic_micros)
58
+ @timestamp = Util.micros
59
+ @clock_start = clock_start
60
+ self
61
+ end
62
+
63
+ def stop(clock_end = Util.monotonic_micros)
64
+ raise 'Transaction not yet start' unless timestamp
65
+ @duration = clock_end - @clock_start
66
+ self
67
+ end
68
+
69
+ def done(result = nil, clock_end: Util.monotonic_micros)
70
+ stop clock_end
71
+ self.result = result if result
72
+ self
73
+ end
74
+
75
+ # spans
76
+
77
+ def inc_started_spans!
78
+ @started_spans += 1
79
+ end
80
+
81
+ def inc_dropped_spans!
82
+ @dropped_spans += 1
83
+ end
84
+
85
+ def max_spans_reached?(config)
86
+ started_spans > config.transaction_max_spans
87
+ end
88
+
89
+ # context
90
+
91
+ def add_response(*args)
92
+ context.response = Context::Response.new(*args)
93
+ end
94
+
95
+ def inspect
96
+ "<Atatus::Transaction id:#{id}" \
97
+ " name:#{name.inspect} type:#{type.inspect}>"
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atatus/metadata'
4
+ require 'atatus/transport/connection'
5
+ require 'atatus/transport/worker'
6
+ require 'atatus/transport/serializers'
7
+ require 'atatus/transport/filters'
8
+ require 'atatus/util/throttle'
9
+
10
+ module Atatus
11
+ module Transport
12
+ # rubocop:disable Metrics/ClassLength
13
+ # @api private
14
+ class Base
15
+ include Logging
16
+
17
+ WATCHER_EXECUTION_INTERVAL = 5
18
+ WATCHER_TIMEOUT_INTERVAL = 4
19
+ WORKER_JOIN_TIMEOUT = 5
20
+
21
+ def initialize(config)
22
+ @config = config
23
+ @queue = SizedQueue.new(config.api_buffer_size)
24
+
25
+ @serializers = Serializers.new(config)
26
+ @filters = Filters.new(config)
27
+
28
+ @stopped = Concurrent::AtomicBoolean.new
29
+ @workers = Array.new(config.pool_size)
30
+
31
+ @watcher_mutex = Mutex.new
32
+ @worker_mutex = Mutex.new
33
+ end
34
+
35
+ attr_reader :config, :queue, :filters, :workers, :watcher, :stopped
36
+
37
+ def start
38
+ debug '%s: Starting Transport', pid_str
39
+
40
+ ensure_watcher_running
41
+ ensure_worker_count
42
+ end
43
+
44
+ def stop
45
+ debug '%s: Stopping Transport', pid_str
46
+
47
+ @stopped.make_true
48
+
49
+ stop_watcher
50
+ stop_workers
51
+ end
52
+
53
+ # rubocop:disable Metrics/MethodLength
54
+ def submit(resource)
55
+ if @stopped.true?
56
+ warn '%s: Transport stopping, no new events accepted', pid_str
57
+ return false
58
+ end
59
+
60
+ ensure_watcher_running
61
+ queue.push(resource, true)
62
+
63
+ true
64
+ rescue ThreadError
65
+ throttled_queue_full_warning
66
+ nil
67
+ rescue Exception => e
68
+ error '%s: Failed adding to the transport queue: %p', pid_str, e.inspect
69
+ nil
70
+ end
71
+ # rubocop:enable Metrics/MethodLength
72
+
73
+ def add_filter(key, callback)
74
+ @filters.add(key, callback)
75
+ end
76
+
77
+ private
78
+
79
+ def pid_str
80
+ format('[PID:%s]', Process.pid)
81
+ end
82
+
83
+ def ensure_watcher_running
84
+ # pid has changed == we've forked
85
+ return if @pid == Process.pid
86
+
87
+ @watcher_mutex.synchronize do
88
+ return if @pid == Process.pid
89
+ @pid = Process.pid
90
+
91
+ @watcher = Concurrent::TimerTask.execute(
92
+ execution_interval: WATCHER_EXECUTION_INTERVAL,
93
+ timeout_interval: WATCHER_TIMEOUT_INTERVAL
94
+ ) { ensure_worker_count }
95
+ end
96
+ end
97
+
98
+ def ensure_worker_count
99
+ @worker_mutex.synchronize do
100
+ return if all_workers_alive?
101
+ return if stopped.true?
102
+
103
+ @workers.map! do |thread|
104
+ next thread if thread&.alive?
105
+
106
+ boot_worker
107
+ end
108
+ end
109
+ end
110
+
111
+ def all_workers_alive?
112
+ !!workers.all? { |t| t&.alive? }
113
+ end
114
+
115
+ def boot_worker
116
+ debug '%s: Booting worker...', pid_str
117
+
118
+ Thread.new do
119
+ Worker.new(
120
+ config, queue,
121
+ serializers: @serializers,
122
+ filters: @filters
123
+ ).work_forever
124
+ end
125
+ end
126
+
127
+ # rubocop:disable Metrics/MethodLength
128
+ def stop_workers
129
+ debug '%s: Stopping workers', pid_str
130
+
131
+ send_stop_messages
132
+
133
+ @worker_mutex.synchronize do
134
+ workers.each do |thread|
135
+ next if thread.nil?
136
+ next if thread.join(WORKER_JOIN_TIMEOUT)
137
+
138
+ debug(
139
+ '%s: Worker did not stop in %ds, killing...',
140
+ pid_str, WORKER_JOIN_TIMEOUT
141
+ )
142
+ thread.kill
143
+ end
144
+
145
+ @workers.clear
146
+ end
147
+ end
148
+ # rubocop:enable Metrics/MethodLength
149
+
150
+ def send_stop_messages
151
+ config.pool_size.times { queue.push(Worker::StopMessage.new, true) }
152
+ rescue ThreadError
153
+ warn 'Cannot push stop messages to worker queue as it is full'
154
+ end
155
+
156
+ def stop_watcher
157
+ @watcher_mutex.synchronize do
158
+ return if watcher.nil? || @pid != Process.pid
159
+ watcher.shutdown
160
+ end
161
+ end
162
+
163
+ def throttled_queue_full_warning
164
+ (@queue_full_log ||= Util::Throttle.new(5) do
165
+ warn(
166
+ '%s: Queue is full (%i items), skipping…',
167
+ pid_str, config.api_buffer_size
168
+ )
169
+ end).call
170
+ end
171
+ end
172
+ # rubocop:enable Metrics/ClassLength
173
+ end
174
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'zlib'
5
+
6
+ require 'atatus/transport/connection/http'
7
+
8
+ module Atatus
9
+ module Transport
10
+ # rubocop:disable Metrics/ClassLength
11
+ # @api private
12
+ class Connection
13
+ include Logging
14
+
15
+ # A connection holds an instance `http` of an Http::Connection.
16
+ #
17
+ # The HTTP::Connection itself is not thread safe.
18
+ #
19
+ # The connection sends write requests and close requests to `http`, and
20
+ # has to ensure no write requests are sent after closing `http`.
21
+ #
22
+ # The connection schedules a separate thread to close an `http`
23
+ # connection some time in the future. To avoid the thread interfering
24
+ # with ongoing write requests to `http`, write and close
25
+ # requests have to be synchronized.
26
+
27
+ HEADERS = {
28
+ 'Content-Type' => 'application/x-ndjson',
29
+ 'Transfer-Encoding' => 'chunked'
30
+ }.freeze
31
+ GZIP_HEADERS = HEADERS.merge(
32
+ 'Content-Encoding' => 'gzip'
33
+ ).freeze
34
+
35
+ def initialize(config, metadata)
36
+ @config = config
37
+ @headers = build_headers(metadata)
38
+ @metadata = JSON.fast_generate(metadata)
39
+ @url = config.server_url + '/intake/v2/events'
40
+ @ssl_context = build_ssl_context
41
+ @mutex = Mutex.new
42
+ end
43
+
44
+ attr_reader :http
45
+
46
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
47
+ def write(str)
48
+ return false if @config.disable_send
49
+
50
+ begin
51
+ bytes_written = 0
52
+
53
+ # The request might get closed from timertask so let's make sure we
54
+ # hold it open until we've written.
55
+ @mutex.synchronize do
56
+ connect if http.nil? || http.closed?
57
+ bytes_written = http.write(str)
58
+ end
59
+
60
+ flush(:api_request_size) if bytes_written >= @config.api_request_size
61
+ rescue IOError => e
62
+ error('Connection error: %s', e.inspect)
63
+ flush(:ioerror)
64
+ rescue Errno::EPIPE => e
65
+ error('Connection error: %s', e.inspect)
66
+ flush(:broken_pipe)
67
+ rescue Exception => e
68
+ error('Connection error: %s', e.inspect)
69
+ flush(:connection_error)
70
+ end
71
+ end
72
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
73
+
74
+ def flush(reason = :force)
75
+ # Could happen from the timertask so we need to sync
76
+ @mutex.synchronize do
77
+ return if http.nil?
78
+ http.close(reason)
79
+ end
80
+ end
81
+
82
+ def inspect
83
+ format(
84
+ '@%s http connection closed? :%s>',
85
+ super.split.first,
86
+ http.closed?
87
+ )
88
+ end
89
+
90
+ private
91
+
92
+ def connect
93
+ schedule_closing if @config.api_request_time
94
+
95
+ @http =
96
+ Http.open(
97
+ @config, @url,
98
+ headers: @headers,
99
+ ssl_context: @ssl_context
100
+ ).tap { |http| http.write(@metadata) }
101
+ end
102
+ # rubocop:enable
103
+
104
+ def schedule_closing
105
+ @close_task&.cancel
106
+ @close_task =
107
+ Concurrent::ScheduledTask.execute(@config.api_request_time) do
108
+ flush(:timeout)
109
+ end
110
+ end
111
+
112
+ def build_headers(metadata)
113
+ (
114
+ @config.http_compression? ? GZIP_HEADERS : HEADERS
115
+ ).dup.tap do |headers|
116
+ headers['User-Agent'] = build_user_agent(metadata)
117
+
118
+ if (token = @config.secret_token)
119
+ headers['Authorization'] = "Bearer #{token}"
120
+ end
121
+ end
122
+ end
123
+
124
+ def build_user_agent(metadata)
125
+ runtime = metadata.dig(:metadata, :service, :runtime)
126
+
127
+ [
128
+ "atatus-ruby/#{VERSION}",
129
+ HTTP::Request::USER_AGENT,
130
+ [runtime[:name], runtime[:version]].join('/')
131
+ ].join(' ')
132
+ end
133
+
134
+ def build_ssl_context # rubocop:disable Metrics/MethodLength
135
+ return unless @config.use_ssl?
136
+
137
+ OpenSSL::SSL::SSLContext.new.tap do |context|
138
+ if @config.server_ca_cert
139
+ context.ca_file = @config.server_ca_cert
140
+ else
141
+ context.cert_store =
142
+ OpenSSL::X509::Store.new.tap(&:set_default_paths)
143
+ end
144
+
145
+ context.verify_mode =
146
+ if @config.verify_server_cert
147
+ OpenSSL::SSL::VERIFY_PEER
148
+ else
149
+ OpenSSL::SSL::VERIFY_NONE
150
+ end
151
+ end
152
+ end
153
+ end
154
+ # rubocop:enable Metrics/ClassLength
155
+ end
156
+ end