hoss-agent 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
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