atatus 1.0.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 (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