hoss-agent 1.0.6

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/Bug_report.md +40 -0
  3. data/.github/ISSUE_TEMPLATE/Feature_request.md +17 -0
  4. data/.github/PULL_REQUEST_TEMPLATE.md +60 -0
  5. data/.gitignore +27 -0
  6. data/.rspec +2 -0
  7. data/Dockerfile +43 -0
  8. data/Gemfile +105 -0
  9. data/LICENSE +201 -0
  10. data/hoss-agent.gemspec +42 -0
  11. data/lib/hoss-agent.rb +210 -0
  12. data/lib/hoss.rb +21 -0
  13. data/lib/hoss/agent.rb +235 -0
  14. data/lib/hoss/central_config.rb +184 -0
  15. data/lib/hoss/central_config/cache_control.rb +51 -0
  16. data/lib/hoss/child_durations.rb +64 -0
  17. data/lib/hoss/config.rb +313 -0
  18. data/lib/hoss/config/bytes.rb +42 -0
  19. data/lib/hoss/config/duration.rb +40 -0
  20. data/lib/hoss/config/options.rb +154 -0
  21. data/lib/hoss/config/regexp_list.rb +30 -0
  22. data/lib/hoss/config/wildcard_pattern_list.rb +54 -0
  23. data/lib/hoss/context.rb +64 -0
  24. data/lib/hoss/context/request.rb +28 -0
  25. data/lib/hoss/context/request/socket.rb +36 -0
  26. data/lib/hoss/context/request/url.rb +59 -0
  27. data/lib/hoss/context/response.rb +47 -0
  28. data/lib/hoss/context/user.rb +59 -0
  29. data/lib/hoss/context_builder.rb +112 -0
  30. data/lib/hoss/deprecations.rb +39 -0
  31. data/lib/hoss/error.rb +49 -0
  32. data/lib/hoss/error/exception.rb +70 -0
  33. data/lib/hoss/error/log.rb +41 -0
  34. data/lib/hoss/error_builder.rb +90 -0
  35. data/lib/hoss/event.rb +131 -0
  36. data/lib/hoss/instrumenter.rb +107 -0
  37. data/lib/hoss/internal_error.rb +23 -0
  38. data/lib/hoss/logging.rb +70 -0
  39. data/lib/hoss/metadata.rb +36 -0
  40. data/lib/hoss/metadata/process_info.rb +35 -0
  41. data/lib/hoss/metadata/service_info.rb +76 -0
  42. data/lib/hoss/metadata/system_info.rb +47 -0
  43. data/lib/hoss/metadata/system_info/container_info.rb +136 -0
  44. data/lib/hoss/naively_hashable.rb +38 -0
  45. data/lib/hoss/rails.rb +68 -0
  46. data/lib/hoss/railtie.rb +42 -0
  47. data/lib/hoss/report.rb +9 -0
  48. data/lib/hoss/sinatra.rb +53 -0
  49. data/lib/hoss/spies.rb +104 -0
  50. data/lib/hoss/spies/faraday.rb +102 -0
  51. data/lib/hoss/spies/http.rb +81 -0
  52. data/lib/hoss/spies/net_http.rb +97 -0
  53. data/lib/hoss/stacktrace.rb +33 -0
  54. data/lib/hoss/stacktrace/frame.rb +66 -0
  55. data/lib/hoss/stacktrace_builder.rb +124 -0
  56. data/lib/hoss/transport/base.rb +191 -0
  57. data/lib/hoss/transport/connection.rb +55 -0
  58. data/lib/hoss/transport/connection/http.rb +139 -0
  59. data/lib/hoss/transport/connection/proxy_pipe.rb +94 -0
  60. data/lib/hoss/transport/filters.rb +60 -0
  61. data/lib/hoss/transport/filters/hash_sanitizer.rb +77 -0
  62. data/lib/hoss/transport/filters/secrets_filter.rb +48 -0
  63. data/lib/hoss/transport/headers.rb +74 -0
  64. data/lib/hoss/transport/serializers.rb +113 -0
  65. data/lib/hoss/transport/serializers/context_serializer.rb +112 -0
  66. data/lib/hoss/transport/serializers/error_serializer.rb +92 -0
  67. data/lib/hoss/transport/serializers/event_serializer.rb +73 -0
  68. data/lib/hoss/transport/serializers/metadata_serializer.rb +92 -0
  69. data/lib/hoss/transport/serializers/report_serializer.rb +33 -0
  70. data/lib/hoss/transport/user_agent.rb +48 -0
  71. data/lib/hoss/transport/worker.rb +326 -0
  72. data/lib/hoss/util.rb +54 -0
  73. data/lib/hoss/util/inflector.rb +110 -0
  74. data/lib/hoss/util/lru_cache.rb +65 -0
  75. data/lib/hoss/util/throttle.rb +52 -0
  76. data/lib/hoss/version.rb +22 -0
  77. metadata +147 -0
@@ -0,0 +1,33 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ module Hoss
21
+ module Transport
22
+ module Serializers
23
+ # @api private
24
+ class ReportSerializer < Serializer
25
+ def build(report)
26
+ {
27
+ events: report.events
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ module Hoss
21
+ module Transport
22
+ # @api private
23
+ class UserAgent
24
+ def initialize(config)
25
+ @built = build(config)
26
+ end
27
+
28
+ def to_s
29
+ @built
30
+ end
31
+
32
+ private
33
+
34
+ def build(config)
35
+ metadata = Metadata.new(config)
36
+
37
+ [
38
+ "hoss-ruby/#{VERSION}",
39
+ HTTP::Request::USER_AGENT,
40
+ [
41
+ metadata.service.runtime.name,
42
+ metadata.service.runtime.version
43
+ ].join('/')
44
+ ].join(' ')
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,326 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ require 'hoss/report'
21
+ require 'zlib'
22
+ require 'cgi'
23
+
24
+ module Hoss
25
+ module Transport
26
+ # @api private
27
+ class Worker
28
+ include Logging
29
+
30
+ class << self
31
+ def adapter
32
+ @adapter ||= Connection
33
+ end
34
+
35
+ attr_writer :adapter
36
+ end
37
+
38
+ # @api private
39
+ class StopMessage; end
40
+
41
+ # @api private
42
+ class FlushMessage; end
43
+
44
+ def initialize(
45
+ config,
46
+ queue,
47
+ serializers:,
48
+ filters:
49
+ )
50
+ @config = config
51
+ @queue = queue
52
+
53
+ @serializers = serializers
54
+ @filters = filters
55
+
56
+ @connection = self.class.adapter.new(config)
57
+ end
58
+
59
+ attr_reader :config, :queue, :filters, :name, :connection, :serializers
60
+ def work_forever
61
+
62
+ done = false
63
+ while (!done && msg = queue.pop)
64
+ begin
65
+ debug 'working message', msg
66
+
67
+ # wait if we don't have a config
68
+ while config.agentConfig.nil?
69
+ sleep 0.1
70
+ end
71
+
72
+ case msg
73
+ when StopMessage
74
+ debug 'Stopping worker -- %s', self
75
+ done = true
76
+ break
77
+ else
78
+ batch = []
79
+ current_batch_size = 0
80
+
81
+ # Use this as the first message in the batch
82
+ event = msg.filtered ? msg : filter_resource(msg)
83
+ unless event.nil?
84
+ event_size = resource_size(event)
85
+ if current_batch_size + event_size <= @config.batch_size
86
+ unless host_blacklisted(event)
87
+ current_batch_size += event_size
88
+ if event.retries < @config.max_event_retries
89
+ batch.push(event)
90
+ else
91
+ debug "max retries hit for event"
92
+ end
93
+ end
94
+ else
95
+ debug "Event is too large, body needs to be truncated"
96
+ end
97
+ end
98
+
99
+ # Do inner loop reading queue to build report
100
+ requeue_messages = []
101
+ while current_batch_size < @config.batch_size && !queue.empty?
102
+ next_msg = queue.pop
103
+ case next_msg
104
+ when StopMessage
105
+ debug 'Stopping worker -- %s', self
106
+ done = true
107
+ break
108
+ else
109
+ event = next_msg.filtered ? next_msg : filter_resource(next_msg)
110
+ unless event.nil?
111
+ event_size = resource_size(event)
112
+ if current_batch_size + event_size <= @config.batch_size
113
+ unless host_blacklisted(event)
114
+ current_batch_size += event_size
115
+ if event.retries < @config.max_event_retries
116
+ batch.push(event)
117
+ else
118
+ debug "max retries hit for event"
119
+ end
120
+ end
121
+ else
122
+ debug "Event too large for this batch, requeue"
123
+ requeue_messages.push(event)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ if batch.length == 0
130
+ debug "batch is empty, breaking"
131
+ break
132
+ end
133
+
134
+ debug "Requeue #{requeue_messages.length} messages" if requeue_messages.length > 0
135
+ requeue_messages.each {|msg| queue.push(msg, false) }
136
+
137
+ report = Report.new
138
+ report.events = batch.map {|event| serializers.serialize(event) }
139
+
140
+ debug "Finished building report"
141
+ data = serializers.serialize(report)
142
+ json = JSON.fast_generate(data)
143
+ begin
144
+ debug json
145
+ rescue Exception => e
146
+ debug 'unable to print body'
147
+ puts json if config.debug
148
+ end
149
+ begin
150
+ connection.write(json)
151
+ rescue Exception => e
152
+ error format('Failed send report: %s %s', e.inspect, e.backtrace)
153
+ batch.each do |m|
154
+ m.retries += 1
155
+ queue.push(m, false)
156
+ end
157
+ sleep 1
158
+ end
159
+ end
160
+ rescue Exception => e
161
+ debug "error in worker #{e.inspect}"
162
+ end
163
+ end
164
+ rescue Exception => e
165
+ warn 'Worker died with exception: %s', e.inspect
166
+ debug e.backtrace.join("\n")
167
+ end
168
+
169
+ private
170
+
171
+ def resource_size(resource)
172
+ size = 20
173
+ size += JSON.fast_generate(resource.request.headers).length if resource.request && resource.request.headers
174
+ size += JSON.fast_generate(resource.response.headers).length if resource.response && resource.response.headers
175
+ size += resource.request.body.length if resource.request && resource.request.body
176
+ size += resource.response.body.length if resource.response && resource.response.body
177
+ size
178
+ end
179
+
180
+ def decompress_body(body, encoding)
181
+ return body unless body
182
+ case (encoding || '').downcase
183
+ when "gzip"
184
+ Zlib::GzipReader.new(StringIO.new(body)).read.to_s
185
+ when "deflate"
186
+ Zlib::Inflate.new.inflate(body)
187
+ else
188
+ body
189
+ end
190
+ rescue Exception => e
191
+ debug "unable to decompress body #{e.inspect}"
192
+ return nil
193
+ end
194
+
195
+ def get_header(headers, name)
196
+ headers[headers.keys.select{|h| h.casecmp(name) == 0}[0]]
197
+ end
198
+
199
+ def filter_resource(resource)
200
+ # Make sure the body is not compressed
201
+ resource.request.body = decompress_body(resource.request.body, get_header(resource.request.headers, "content-encoding")) if resource.request
202
+ resource.response.body = decompress_body(resource.response.body, get_header(resource.response.headers,"content-encoding")) if resource.response
203
+
204
+ # Start applying filters
205
+ config = get_configuration_for_host(resource.request.headers['host'])
206
+ debug "Using config for event #{config.to_s}"
207
+
208
+ # Filter the headers
209
+ filter_headers(resource.request.headers, config['sanitizedHeaders']) if resource.request
210
+ filter_headers(resource.response.headers, config['sanitizedHeaders']) if resource.response
211
+
212
+ # Filter the body
213
+ if config['bodyCapture'] == 'Off' || (config['bodyCapture'] == 'OnError' && !contains_error(resource))
214
+ resource.request.body = nil if resource.request
215
+ resource.response.body = nil if resource.response
216
+ else
217
+ resource.request.body = filter_body(resource.request.body, config['sanitizedBodyFields']) if resource.request
218
+ resource.response.body = filter_body(resource.response.body, config['sanitizedBodyFields']) if resource.response
219
+ end
220
+
221
+ # Filter the query params
222
+ resource.request.url = filter_url(resource.request.url, config['sanitizedQueryParams'])
223
+
224
+ # Record this event as filtered in case of retry
225
+ resource.filtered = true
226
+
227
+ resource
228
+ end
229
+
230
+ def contains_error(resource)
231
+ if !resource.response.nil? && resource.response.status_code >= 400
232
+ return true
233
+ end
234
+ false
235
+ end
236
+
237
+ def filter_url(url, config)
238
+ uri = URI(url)
239
+ if uri.query
240
+ query = CGI::parse(uri.query)
241
+ query.keys.each{|k| query[k] = 'xxx' if contains_string(config, k)}
242
+ uri.query = URI.encode_www_form(query)
243
+ return uri.to_s
244
+ end
245
+ url
246
+ rescue Exception => e
247
+ error "Error filtering url", e
248
+ url
249
+ end
250
+
251
+ def filter_headers(headers, names)
252
+ headers.each { |h| headers[h[0]] = 'xxx' if contains_string(names, h[0]) }
253
+ end
254
+
255
+ def filter_body(body, fields)
256
+ return nil if body.nil?
257
+ json = JSON.parse(body)
258
+ JSON.fast_generate(mask_object_fields(json, fields))
259
+ rescue Exception => e
260
+ body
261
+ end
262
+
263
+ def mask_object_fields(obj, fields)
264
+ field_names = fields.map {|f| f['value'] }
265
+ case obj
266
+ when Hash
267
+ obj.keys.each do |k|
268
+ if contains_string(field_names, k)
269
+ obj[k] = 'xxx'
270
+ else
271
+ obj[k] = mask_object_fields(obj[k], fields)
272
+ end
273
+ end
274
+ when Array
275
+ obj.each_index do |i|
276
+ obj[i] = mask_object_fields(obj[i], fields) if (obj[i].is_a?(Hash) || obj[i].is_a?(Array))
277
+ end
278
+ end
279
+ obj
280
+ end
281
+
282
+ # Get the configuration for a host, either use a defined api or the default account config
283
+ def get_configuration_for_host(host)
284
+ api_config = api_configurations.select{ |api| contains_string(api['hosts'], host)}[0]
285
+ return api_config['configuration'] if api_config
286
+ account_api_configuration
287
+ end
288
+
289
+ # Parse out the host blacklist
290
+ def get_account_host_blacklist
291
+ account_api_configuration['hostBlacklist']
292
+ end
293
+
294
+ # Do a case insensitive search for a string in array
295
+ def contains_string(arr, a)
296
+ !arr.select { |b| b.casecmp(a) == 0 }.empty?
297
+ end
298
+
299
+ # Return 'accountApiConfiguration' or empty object
300
+ def account_api_configuration
301
+ return {} if config.agentConfig.nil? or config.agentConfig['accountApiConfiguration'].nil?
302
+ config.agentConfig['accountApiConfiguration']
303
+ end
304
+
305
+ # Return the 'apis' or empty object
306
+ def api_configurations
307
+ return {} if config.agentConfig.nil? or config.agentConfig['apis'].nil?
308
+ config.agentConfig['apis']
309
+ end
310
+
311
+ def host_blacklisted(event)
312
+ blacklist = get_account_host_blacklist
313
+ return false if blacklist.nil?
314
+ is_blacklisted = false
315
+ unless event.request.nil? || event.request.url.nil?
316
+ url = URI(event.request.url)
317
+ blacklist.each do |host|
318
+ is_blacklisted = true if url.host == host
319
+ is_blacklisted = true if url.host.end_with? '.'+host
320
+ end
321
+ end
322
+ is_blacklisted
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,54 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ module Hoss
21
+ # @api private
22
+ module Util
23
+ def self.micros(target = Time.now)
24
+ utc = target.utc
25
+ utc.to_i * 1_000_000 + utc.usec
26
+ end
27
+
28
+ def self.monotonic_micros
29
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
30
+ end
31
+
32
+ def self.git_sha
33
+ sha = `git rev-parse --verify HEAD 2>&1`.chomp
34
+ $?&.success? ? sha : nil
35
+ end
36
+
37
+ def self.hex_to_bits(str)
38
+ str.hex.to_s(2).rjust(str.size * 4, '0')
39
+ end
40
+
41
+ def self.reverse_merge!(first, *others)
42
+ others.reduce(first) do |curr, other|
43
+ curr.merge!(other) { |_, _, new| new }
44
+ end
45
+ end
46
+
47
+ def self.truncate(value, max_length: 1024)
48
+ return unless value
49
+ return value if value.length <= max_length
50
+
51
+ value[0...(max_length - 1)] + '…'
52
+ end
53
+ end
54
+ end