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,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atatus/error'
4
+
5
+ require 'atatus/context_builder'
6
+ require 'atatus/error_builder'
7
+ require 'atatus/stacktrace_builder'
8
+ require 'atatus/collector/base'
9
+
10
+ require 'atatus/central_config'
11
+ # require 'atatus/transport/base'
12
+ require 'atatus/metrics'
13
+
14
+ require 'atatus/spies'
15
+
16
+ module Atatus
17
+ # rubocop:disable Metrics/ClassLength
18
+ # @api private
19
+ class Agent
20
+ include Logging
21
+
22
+ LOCK = Mutex.new
23
+
24
+ # life cycle
25
+
26
+ def self.instance # rubocop:disable Style/TrivialAccessors
27
+ @instance
28
+ end
29
+
30
+ # rubocop:disable Metrics/MethodLength
31
+ def self.start(config)
32
+ return @instance if @instance
33
+
34
+ config = Config.new(config) unless config.is_a?(Config)
35
+
36
+ LOCK.synchronize do
37
+ return @instance if @instance
38
+
39
+ unless config.active?
40
+ config.logger.debug format(
41
+ "%sAgent disabled with `active: false'",
42
+ Logging::PREFIX
43
+ )
44
+ return
45
+ end
46
+
47
+ @instance = new(config).start
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/MethodLength
51
+
52
+ def self.stop
53
+ LOCK.synchronize do
54
+ return unless @instance
55
+
56
+ @instance.stop
57
+ @instance = nil
58
+ end
59
+ end
60
+
61
+ def self.running?
62
+ !!@instance
63
+ end
64
+
65
+ # rubocop:disable Metrics/MethodLength
66
+ def initialize(config)
67
+ @config = config
68
+
69
+ @stacktrace_builder = StacktraceBuilder.new(config)
70
+ @context_builder = ContextBuilder.new(config)
71
+ @error_builder = ErrorBuilder.new(self)
72
+
73
+ @central_config = CentralConfig.new(config)
74
+ # @transport = Transport::Base.new(config)
75
+ @instrumenter = Instrumenter.new(
76
+ config,
77
+ stacktrace_builder: stacktrace_builder
78
+ ) { |event| enqueue event }
79
+ @metrics = Metrics.new(config) { |event| enqueue event }
80
+ @collector = Collector::Base.new(config)
81
+ end
82
+ # rubocop:enable Metrics/MethodLength
83
+
84
+ attr_reader(
85
+ :central_config,
86
+ :config,
87
+ :context_builder,
88
+ :error_builder,
89
+ :instrumenter,
90
+ :metrics,
91
+ :stacktrace_builder,
92
+ # :transport,
93
+ :collector
94
+ )
95
+
96
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
97
+ def start
98
+ unless config.disable_start_message
99
+ info '[%s] Starting atatus ruby agent', VERSION
100
+ end
101
+
102
+ if @config.license_key.nil? || @config.app_name.nil?
103
+ if @config.license_key.nil? && @config.app_name.nil?
104
+ error 'Atatus configuration license_key and app_name are missing'
105
+ elsif @config.license_key.nil?
106
+ error 'Atatus configuration license_key is missing'
107
+ elsif @config.app_name.nil?
108
+ error 'Atatus configuration app_name is missing'
109
+ end
110
+ end
111
+
112
+ collector.start
113
+ central_config.start
114
+ # transport.start
115
+ instrumenter.start
116
+ metrics.start
117
+
118
+ config.enabled_instrumentations.each do |lib|
119
+ debug "Requiring spy: #{lib}"
120
+ require "atatus/spies/#{lib}"
121
+ end
122
+
123
+ self
124
+ end
125
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
126
+
127
+ def stop
128
+ debug 'Stopping agent'
129
+
130
+ central_config.stop
131
+ metrics.stop
132
+ instrumenter.stop
133
+ # transport.stop
134
+
135
+ self
136
+ end
137
+
138
+ at_exit do
139
+ stop
140
+ end
141
+
142
+ # transport
143
+
144
+ def enqueue(obj)
145
+ # transport.submit obj
146
+ case obj
147
+ when Atatus::Transaction
148
+ collector.add_txn(obj)
149
+ when Atatus::Span
150
+ collector.add_span(obj)
151
+ when Atatus::Error
152
+ collector.add_error(obj)
153
+ when Atatus::Metricset
154
+ collector.add_metrics(obj)
155
+ end
156
+ end
157
+
158
+ # instrumentation
159
+
160
+ def current_transaction
161
+ instrumenter.current_transaction
162
+ end
163
+
164
+ def current_span
165
+ instrumenter.current_span
166
+ end
167
+
168
+ def start_transaction(
169
+ name = nil,
170
+ type = nil,
171
+ context: nil,
172
+ trace_context: nil
173
+ )
174
+ instrumenter.start_transaction(
175
+ name,
176
+ type,
177
+ context: context,
178
+ trace_context: trace_context
179
+ )
180
+ end
181
+
182
+ def end_transaction(result = nil)
183
+ instrumenter.end_transaction(result)
184
+ end
185
+
186
+ # rubocop:disable Metrics/ParameterLists
187
+ def start_span(
188
+ name = nil,
189
+ type = nil,
190
+ subtype: nil,
191
+ action: nil,
192
+ backtrace: nil,
193
+ context: nil,
194
+ trace_context: nil
195
+ )
196
+ instrumenter.start_span(
197
+ name,
198
+ type,
199
+ subtype: subtype,
200
+ action: action,
201
+ backtrace: backtrace,
202
+ context: context,
203
+ trace_context: trace_context
204
+ )
205
+ end
206
+ # rubocop:enable Metrics/ParameterLists
207
+
208
+ def end_span
209
+ instrumenter.end_span
210
+ end
211
+
212
+ def set_label(key, value)
213
+ instrumenter.set_label(key, value)
214
+ end
215
+
216
+ def set_custom_context(context)
217
+ instrumenter.set_custom_context(context)
218
+ end
219
+
220
+ def set_user(user)
221
+ instrumenter.set_user(user)
222
+ end
223
+
224
+ def build_context(rack_env:, for_type:)
225
+ @context_builder.build(rack_env: rack_env, for_type: for_type)
226
+ end
227
+
228
+ # errors
229
+
230
+ def report(exception, context: nil, handled: true)
231
+ return if config.filter_exception_types.include?(exception.class.to_s)
232
+
233
+ error = @error_builder.build_exception(
234
+ exception,
235
+ context: context,
236
+ handled: handled
237
+ )
238
+ enqueue error
239
+ error.id
240
+ end
241
+
242
+ def report_message(message, context: nil, backtrace: nil, **attrs)
243
+ error = @error_builder.build_log(
244
+ message,
245
+ context: context,
246
+ backtrace: backtrace,
247
+ **attrs
248
+ )
249
+ enqueue error
250
+ error.id
251
+ end
252
+
253
+ # filters
254
+
255
+ def add_filter(key, callback)
256
+ # transport.add_filter(key, callback)
257
+ end
258
+ end
259
+ # rubocop:enable Metrics/ClassLength
260
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atatus/central_config/cache_control'
4
+
5
+ module Atatus
6
+ # @api private
7
+ class CentralConfig
8
+ include Logging
9
+
10
+ # @api private
11
+ class ResponseError < InternalError
12
+ def initialize(response)
13
+ @response = response
14
+ end
15
+
16
+ attr_reader :response
17
+ end
18
+ class ClientError < ResponseError; end
19
+ class ServerError < ResponseError; end
20
+
21
+ DEFAULT_MAX_AGE = 300
22
+
23
+ def initialize(config)
24
+ @config = config
25
+ @modified_options = {}
26
+ @service_info = {
27
+ 'service.name': config.service_name,
28
+ 'service.environment': config.environment
29
+ }.to_json
30
+ end
31
+
32
+ attr_reader :config
33
+ attr_reader :scheduled_task, :promise # for specs
34
+
35
+ def start
36
+ return unless config.central_config?
37
+
38
+ fetch_and_apply_config
39
+ end
40
+
41
+ def fetch_and_apply_config
42
+ @promise =
43
+ Concurrent::Promise
44
+ .execute(&method(:fetch_config))
45
+ .on_success(&method(:handle_success))
46
+ .rescue(&method(:handle_error))
47
+ end
48
+
49
+ def stop
50
+ @scheduled_task&.cancel
51
+ end
52
+
53
+ # rubocop:disable Metrics/MethodLength
54
+ def fetch_config
55
+ resp = perform_request
56
+
57
+ case resp.status
58
+ when 200..299
59
+ resp
60
+ when 300..399
61
+ resp
62
+ when 400..499
63
+ raise ClientError, resp
64
+ when 500..599
65
+ raise ServerError, resp
66
+ end
67
+ end
68
+ # rubocop:enable Metrics/MethodLength
69
+
70
+ def assign(update)
71
+ # For each updated option, store the original value,
72
+ # unless already stored
73
+ update.each_key do |key|
74
+ @modified_options[key] ||= config.get(key.to_sym)&.value
75
+ end
76
+
77
+ # If the new update doesn't set a previously modified option,
78
+ # revert it to the original
79
+ @modified_options.each_key do |key|
80
+ next if update.key?(key)
81
+ update[key] = @modified_options.delete(key)
82
+ end
83
+
84
+ config.assign(update)
85
+ end
86
+
87
+ private
88
+
89
+ # rubocop:disable Metrics/MethodLength
90
+ def handle_success(resp)
91
+ if resp.status == 304
92
+ info 'Received 304 Not Modified'
93
+ else
94
+ update = JSON.parse(resp.body.to_s)
95
+ assign(update)
96
+
97
+ info 'Updated config from Kibana'
98
+ end
99
+
100
+ schedule_next_fetch(resp)
101
+
102
+ true
103
+ rescue Exception => e
104
+ error 'Failed to apply remote config, %s', e.inspect
105
+ debug { e.backtrace.join('\n') }
106
+ end
107
+ # rubocop:enable Metrics/MethodLength
108
+
109
+ def handle_error(error)
110
+ error(
111
+ 'Failed fetching config: %s, trying again in %d seconds',
112
+ error.response.body, DEFAULT_MAX_AGE
113
+ )
114
+
115
+ assign({})
116
+
117
+ schedule_next_fetch(error.response)
118
+ end
119
+
120
+ def perform_request
121
+ Http.post(
122
+ config.server_url + '/agent/v1/config/',
123
+ body: @service_info,
124
+ headers: { etag: 1, content_type: 'application/json' }
125
+ )
126
+ end
127
+
128
+ def schedule_next_fetch(resp)
129
+ seconds =
130
+ if (cache_header = resp.headers['Cache-Control'])
131
+ CacheControl.new(cache_header).max_age
132
+ else
133
+ DEFAULT_MAX_AGE
134
+ end
135
+
136
+ @scheduled_task =
137
+ Concurrent::ScheduledTask
138
+ .execute(seconds, &method(:fetch_and_apply_config))
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class CentralConfig
5
+ # @api private
6
+ class CacheControl
7
+ def initialize(value)
8
+ @header = value
9
+ parse!(value)
10
+ end
11
+
12
+ attr_reader(
13
+ :must_revalidate,
14
+ :no_cache,
15
+ :no_store,
16
+ :no_transform,
17
+ :public,
18
+ :private,
19
+ :proxy_revalidate,
20
+ :max_age,
21
+ :s_maxage
22
+ )
23
+
24
+ private
25
+
26
+ def parse!(value)
27
+ value.split(',').each do |token|
28
+ k, v = token.split('=').map(&:strip)
29
+ instance_variable_set(:"@#{k.gsub('-', '_')}", v ? v.to_i : true)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,329 @@
1
+ require 'atatus'
2
+ require 'atatus/transaction'
3
+ require 'atatus/config'
4
+ # require 'atatus/logger'
5
+ require 'atatus/collector/transport'
6
+ require 'json'
7
+
8
+ module Atatus
9
+ module Collector
10
+ class Layer
11
+ attr_reader :count, :min, :max, :total, :type, :kind, :id, :pid
12
+ attr_writer :id, :pid
13
+
14
+ def initialize(type, kind, duration)
15
+ @type = type
16
+ @kind = kind
17
+ @count = 1
18
+ @min = duration
19
+ @max = duration
20
+ @total = duration
21
+ end
22
+
23
+ def aggregate!(duration)
24
+ @count += 1
25
+ @min = duration if duration < @min
26
+ @max = duration if duration > @max
27
+ @total += duration
28
+ end
29
+
30
+ def self.span_kind(value)
31
+ return KINDS.fetch(value, value)
32
+ end
33
+
34
+ def self.span_type(value)
35
+ return TYPES.fetch(value, value)
36
+ end
37
+
38
+ private
39
+ KINDS = {
40
+ 'db' => 'Database',
41
+ 'cache' => 'Database',
42
+ 'ext' => 'Remote',
43
+ 'websocket' => 'Remote',
44
+ 'template' => 'Template'
45
+ }.freeze
46
+
47
+ TYPES = {
48
+ 'mysql' => 'MySQL',
49
+ 'mysql2' => 'MySQL',
50
+ 'postgresql' => 'Postgres',
51
+ 'mssql' => 'MS SQL',
52
+ 'mongodb' => 'MongoDB',
53
+ 'redis' => 'Redis',
54
+ 'graphql' => 'GraphQL',
55
+ 'elasticsearch' => 'Elasticsearch',
56
+ 'cassandra' => 'Cassandra',
57
+ 'http' => 'External Requests',
58
+ 'http2' => 'External Requests',
59
+ 'http_rb' => 'External Requests',
60
+ 'net_http' => 'External Requests'
61
+ }.freeze
62
+ end
63
+
64
+ class Txn < Layer
65
+ attr_reader :spans
66
+
67
+ def initialize(type, kind, duration)
68
+ super(type, kind, duration)
69
+ @spans = Hash.new {}
70
+ end
71
+
72
+ def add_span(span_name, type, kind, duration)
73
+ if !@spans.has_key?(span_name)
74
+ @spans[key] = Layer.new(type, kind, duration)
75
+ else
76
+ @spans[key].aggregate! duration
77
+ end
78
+ end
79
+ end
80
+
81
+ class Base
82
+ include Logging
83
+
84
+ def initialize(config)
85
+ # info 'Initializing Collector'
86
+ @config = config
87
+ @spans = Hash.new {|h,k| h[k]=[]}
88
+ @running = false
89
+ uri = URI("https://apm-rx.atatus.com/")
90
+ @transport = Atatus::BaseTransport.new(config, uri.host, uri.port)
91
+ @txn_lock = Mutex.new
92
+ @txn_aggs = Hash.new {}
93
+ @trace_aggs = Array.new()
94
+ @error_metrics = Hash.new {}
95
+ @error_requests = Array.new()
96
+
97
+ @error_lock = Mutex.new
98
+ @error_aggs = Array.new()
99
+
100
+ @metrics_lock = Mutex.new
101
+ @metrics = Array.new()
102
+ @collect_counter = 0
103
+ end
104
+
105
+ attr_reader :config
106
+
107
+ def pid_str
108
+ format('[PID:%s]', Process.pid)
109
+ end
110
+
111
+ def start
112
+ debug '%s: Starting Collector', pid_str
113
+
114
+ ensure_worker_running
115
+ end
116
+
117
+ def add_error(error)
118
+ ensure_worker_running
119
+
120
+ @error_lock.synchronize do
121
+ if @error_aggs.length < 20
122
+ @error_aggs.push(error)
123
+ else
124
+ i = rand(20)
125
+ @error_aggs[i] = error
126
+ end
127
+ end
128
+ # puts error.to_yaml
129
+ end
130
+
131
+ def add_metrics(metricset)
132
+ ensure_worker_running
133
+ metric = {}
134
+ if metricset.samples.key?(:'system.memory.total')
135
+ metric[:'system.cpu.total.norm.pct'] = metricset.samples[:'system.cpu.total.norm.pct']
136
+ metric[:'system.memory.actual.free'] = metricset.samples[:'system.memory.actual.free']
137
+ metric[:'system.memory.total'] = metricset.samples[:'system.memory.total']
138
+ metric[:'system.process.cpu.total.norm.pct'] = metricset.samples[:'system.process.cpu.total.norm.pct']
139
+ metric[:'system.process.memory.size'] = metricset.samples[:'system.process.memory.size']
140
+ metric[:'system.process.memory.rss.bytes'] = metricset.samples[:'system.process.memory.rss.bytes']
141
+ end
142
+
143
+ if metricset.samples.key?(:'ruby.gc.count')
144
+ metric[:'ruby.gc.count'] = metricset.samples[:'ruby.gc.count']
145
+ metric[:'ruby.threads'] = metricset.samples[:'ruby.threads']
146
+ metric[:'ruby.heap.slots.live'] = metricset.samples[:'ruby.heap.slots.live']
147
+ metric[:'ruby.heap.slots.free'] = metricset.samples[:'ruby.heap.slots.free']
148
+ metric[:'ruby.heap.allocations.total'] = metricset.samples[:'ruby.heap.allocations.total']
149
+ end
150
+
151
+ @metrics_lock.synchronize do
152
+ @metrics << metric
153
+ end
154
+ # puts metric.to_yaml
155
+ end
156
+
157
+ def add_span(span)
158
+ ensure_worker_running
159
+
160
+ @spans[span.transaction_id] << span if span.transaction_id
161
+ # puts span.to_yaml
162
+ end
163
+
164
+ def add_txn(txn)
165
+ ensure_worker_running
166
+
167
+ # puts txn.to_yaml
168
+ txn_name = txn.name
169
+ return unless txn_name
170
+ @txn_lock.synchronize do
171
+ if !@txn_aggs.has_key?(txn_name)
172
+ @txn_aggs[txn_name] = Txn.new(@config.framework_name, "Ruby", txn.duration)
173
+ @txn_aggs[txn_name].id = txn.id
174
+ @txn_aggs[txn_name].pid = txn.id
175
+ else
176
+ @txn_aggs[txn_name].aggregate! txn.duration
177
+ end
178
+
179
+ spans_present = false
180
+ if @spans.has_key?(txn.id)
181
+ @spans[txn.id].each do |span|
182
+ span_name = span.name
183
+ if !@txn_aggs[txn_name].spans.has_key?(span_name)
184
+ kind = Layer.span_kind(span.type)
185
+ type = Layer.span_type(span.subtype)
186
+ @txn_aggs[txn_name].spans[span_name] = Layer.new(type, kind, span.duration)
187
+ @txn_aggs[txn_name].spans[span_name].id = span.id
188
+ @txn_aggs[txn_name].spans[span_name].pid = span.transaction_id
189
+ else
190
+ @txn_aggs[txn_name].spans[span_name].aggregate! span.duration
191
+ end
192
+ end
193
+ spans_present = true
194
+ end
195
+
196
+ if spans_present
197
+ if Util.ms(txn.duration) >= @config.trace_threshold
198
+ trace_txn = txn
199
+ trace_txn.spans = @spans[txn.id]
200
+
201
+ if @trace_aggs.length < 5
202
+ @trace_aggs.push(trace_txn)
203
+ else
204
+ i = rand(5)
205
+ @trace_aggs[i] = trace_txn
206
+ end
207
+ end
208
+ end
209
+
210
+ if spans_present
211
+ @spans.delete(txn.id)
212
+ end
213
+
214
+ status_code = txn.context.response.status_code
215
+
216
+ if status_code >= 400 && status_code != 404
217
+ if !@error_metrics.has_key?(txn_name)
218
+ @error_metrics[txn_name] = {status_code => 1}
219
+ else
220
+ if !@error_metrics[txn_name].has_key?(status_code)
221
+ @error_metrics[txn_name][status_code] = 1
222
+ else
223
+ @error_metrics[txn_name][status_code] += 1
224
+ end
225
+ end
226
+
227
+ # puts txn.context.to_yaml
228
+ if @error_requests.length < 20
229
+ @error_requests.push({'name' => txn_name, 'context' => txn.context})
230
+ else
231
+ i = rand(20)
232
+ @error_requests[i] = {'name' => txn_name, 'context' => txn.context}
233
+ end
234
+ end
235
+
236
+ end
237
+ end
238
+
239
+ private
240
+ def ensure_worker_running
241
+ return if worker_active?
242
+ @running = true
243
+ @worker = Thread.new() do
244
+ debug '%s: Starting Collector Worker', pid_str
245
+
246
+ while @running
247
+ start_time = Time.now
248
+ sleep(60)
249
+ collect start_time
250
+ end
251
+ end
252
+
253
+ at_exit do
254
+ debug '%s: Waiting for Collector Worker to exit', pid_str
255
+ @running = false
256
+ @worker.run
257
+ @worker.join(10)
258
+ end
259
+ end
260
+
261
+ def worker_active?
262
+ @worker && @worker.alive?
263
+ end
264
+
265
+ def collect(start_time)
266
+ # puts @config.to_yaml
267
+ # return
268
+ if @config.license_key.nil? || @config.app_name.nil?
269
+ if @config.license_key.nil? && @config.app_name.nil?
270
+ error '%s: Atatus configuration license_key and app_name are missing', pid_str
271
+ return
272
+ elsif @config.license_key.nil?
273
+ error '%s: Atatus configuration license_key is missing', pid_str
274
+ return
275
+ elsif @config.app_name.nil?
276
+ error '%s: Atatus configuration app_name is missing', pid_str
277
+ return
278
+ end
279
+ end
280
+
281
+ if @collect_counter % 30 == 0
282
+ @transport.hostinfo(start_time)
283
+ @collect_counter = 0
284
+ end
285
+ @collect_counter += 1
286
+
287
+ end_time = Time.now
288
+ debug '%s: Collecting transactions', pid_str
289
+
290
+ txn_data = nil
291
+ trace_data = nil
292
+ error_data = nil
293
+ error_metrics_data = nil
294
+ error_requests_data = nil
295
+ metrics_data = nil
296
+
297
+ @txn_lock.synchronize do
298
+ txn_data = @txn_aggs
299
+ @txn_aggs = Hash.new {}
300
+
301
+ trace_data = @trace_aggs
302
+ @trace_aggs = Array.new()
303
+
304
+ error_metrics_data = @error_metrics
305
+ @error_metrics = Hash.new {}
306
+
307
+ error_requests_data = @error_requests
308
+ @error_requests = Array.new()
309
+ end
310
+
311
+ @error_lock.synchronize do
312
+ error_data = @error_aggs
313
+ @error_aggs = Array.new()
314
+ end
315
+
316
+ @metrics_lock.synchronize do
317
+ metrics_data = @metrics
318
+ @metrics = Array.new()
319
+ end
320
+
321
+ @transport.txn(start_time, end_time, txn_data) unless txn_data.empty?
322
+ @transport.trace(start_time, end_time, trace_data) unless trace_data.empty?
323
+ @transport.error_metrics(start_time, end_time, error_metrics_data, error_requests_data) unless error_metrics_data.empty?
324
+ @transport.errors(start_time, end_time, error_data) unless error_data.empty?
325
+ @transport.metrics(start_time, end_time, metrics_data) unless metrics_data.empty?
326
+ end
327
+ end
328
+ end
329
+ end