hoss-agent 1.0.1

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 +231 -0
  12. data/lib/hoss/central_config/cache_control.rb +51 -0
  13. data/lib/hoss/central_config.rb +184 -0
  14. data/lib/hoss/child_durations.rb +64 -0
  15. data/lib/hoss/config/bytes.rb +42 -0
  16. data/lib/hoss/config/duration.rb +40 -0
  17. data/lib/hoss/config/options.rb +154 -0
  18. data/lib/hoss/config/regexp_list.rb +30 -0
  19. data/lib/hoss/config/wildcard_pattern_list.rb +54 -0
  20. data/lib/hoss/config.rb +304 -0
  21. data/lib/hoss/context/request/socket.rb +36 -0
  22. data/lib/hoss/context/request/url.rb +59 -0
  23. data/lib/hoss/context/request.rb +28 -0
  24. data/lib/hoss/context/response.rb +47 -0
  25. data/lib/hoss/context/user.rb +59 -0
  26. data/lib/hoss/context.rb +64 -0
  27. data/lib/hoss/context_builder.rb +112 -0
  28. data/lib/hoss/deprecations.rb +39 -0
  29. data/lib/hoss/error/exception.rb +70 -0
  30. data/lib/hoss/error/log.rb +41 -0
  31. data/lib/hoss/error.rb +49 -0
  32. data/lib/hoss/error_builder.rb +90 -0
  33. data/lib/hoss/event.rb +131 -0
  34. data/lib/hoss/instrumenter.rb +107 -0
  35. data/lib/hoss/internal_error.rb +23 -0
  36. data/lib/hoss/logging.rb +70 -0
  37. data/lib/hoss/metadata/process_info.rb +35 -0
  38. data/lib/hoss/metadata/service_info.rb +76 -0
  39. data/lib/hoss/metadata/system_info/container_info.rb +136 -0
  40. data/lib/hoss/metadata/system_info.rb +47 -0
  41. data/lib/hoss/metadata.rb +36 -0
  42. data/lib/hoss/naively_hashable.rb +38 -0
  43. data/lib/hoss/rails.rb +68 -0
  44. data/lib/hoss/railtie.rb +42 -0
  45. data/lib/hoss/report.rb +9 -0
  46. data/lib/hoss/sinatra.rb +53 -0
  47. data/lib/hoss/spies/faraday.rb +102 -0
  48. data/lib/hoss/spies/http.rb +81 -0
  49. data/lib/hoss/spies/net_http.rb +97 -0
  50. data/lib/hoss/spies.rb +104 -0
  51. data/lib/hoss/stacktrace/frame.rb +66 -0
  52. data/lib/hoss/stacktrace.rb +33 -0
  53. data/lib/hoss/stacktrace_builder.rb +124 -0
  54. data/lib/hoss/transport/base.rb +191 -0
  55. data/lib/hoss/transport/connection/http.rb +139 -0
  56. data/lib/hoss/transport/connection/proxy_pipe.rb +94 -0
  57. data/lib/hoss/transport/connection.rb +55 -0
  58. data/lib/hoss/transport/filters/hash_sanitizer.rb +77 -0
  59. data/lib/hoss/transport/filters/secrets_filter.rb +48 -0
  60. data/lib/hoss/transport/filters.rb +60 -0
  61. data/lib/hoss/transport/headers.rb +74 -0
  62. data/lib/hoss/transport/serializers/context_serializer.rb +112 -0
  63. data/lib/hoss/transport/serializers/error_serializer.rb +92 -0
  64. data/lib/hoss/transport/serializers/event_serializer.rb +73 -0
  65. data/lib/hoss/transport/serializers/metadata_serializer.rb +92 -0
  66. data/lib/hoss/transport/serializers/report_serializer.rb +33 -0
  67. data/lib/hoss/transport/serializers.rb +113 -0
  68. data/lib/hoss/transport/user_agent.rb +48 -0
  69. data/lib/hoss/transport/worker.rb +319 -0
  70. data/lib/hoss/util/inflector.rb +110 -0
  71. data/lib/hoss/util/lru_cache.rb +65 -0
  72. data/lib/hoss/util/throttle.rb +52 -0
  73. data/lib/hoss/util.rb +54 -0
  74. data/lib/hoss/version.rb +22 -0
  75. data/lib/hoss-agent.rb +210 -0
  76. data/lib/hoss.rb +21 -0
  77. metadata +147 -0
data/lib/hoss/agent.rb ADDED
@@ -0,0 +1,231 @@
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/error'
21
+
22
+ require 'hoss/context_builder'
23
+ require 'hoss/error_builder'
24
+ require 'hoss/stacktrace_builder'
25
+
26
+ require 'hoss/central_config'
27
+ require 'hoss/transport/base'
28
+
29
+ require 'hoss/spies'
30
+
31
+ module Hoss
32
+ # @api private
33
+ class Agent
34
+ include Logging
35
+ extend Forwardable
36
+
37
+ LOCK = Mutex.new
38
+
39
+ # life cycle
40
+
41
+ def self.instance # rubocop:disable Style/TrivialAccessors
42
+ @instance
43
+ end
44
+
45
+ def self.start(config)
46
+ return @instance if @instance
47
+
48
+ config = Config.new(config) unless config.is_a?(Config)
49
+
50
+ LOCK.synchronize do
51
+ return @instance if @instance
52
+
53
+ unless config.enabled?
54
+ config.logger.debug format(
55
+ "%sAgent disabled with `enabled: false'",
56
+ Logging::PREFIX
57
+ )
58
+ return
59
+ end
60
+
61
+ @instance = new(config).start
62
+ end
63
+ end
64
+
65
+ def self.stop
66
+ LOCK.synchronize do
67
+ return unless @instance
68
+
69
+ @instance.stop
70
+ @instance = nil
71
+ end
72
+ end
73
+
74
+ def self.running?
75
+ !!@instance
76
+ end
77
+
78
+ def initialize(config)
79
+ @stacktrace_builder = StacktraceBuilder.new(config)
80
+ @context_builder = ContextBuilder.new(config)
81
+ @error_builder = ErrorBuilder.new(self)
82
+
83
+ @central_config = CentralConfig.new(config)
84
+ @transport = Transport::Base.new(config)
85
+ @instrumenter = Instrumenter.new(
86
+ config,
87
+ metrics: nil,
88
+ stacktrace_builder: stacktrace_builder
89
+ ) { |event| enqueue event }
90
+ @pid = Process.pid
91
+ end
92
+
93
+ attr_reader(
94
+ :central_config,
95
+ :config,
96
+ :context_builder,
97
+ :error_builder,
98
+ :instrumenter,
99
+ :stacktrace_builder,
100
+ :transport
101
+ )
102
+
103
+ def_delegator :@central_config, :config
104
+
105
+ def start
106
+ unless config.disable_start_message?
107
+ config.logger.info format(
108
+ '[%s] Starting agent, reporting to %s',
109
+ VERSION, config.api_host
110
+ )
111
+ end
112
+
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 "hoss/spies/#{lib}"
121
+ end
122
+
123
+ self
124
+ end
125
+
126
+ def stop
127
+ debug 'Stopping agent'
128
+
129
+ central_config.stop
130
+ instrumenter.stop
131
+ transport.stop
132
+
133
+ self
134
+ end
135
+
136
+ at_exit do
137
+ stop
138
+ end
139
+
140
+ # transport
141
+
142
+ def enqueue(obj)
143
+ transport.submit obj
144
+ end
145
+
146
+ def current_event
147
+ instrumenter.current_event
148
+ end
149
+
150
+ def start_event
151
+ detect_forking!
152
+ instrumenter.start_event
153
+ end
154
+
155
+ def end_event
156
+ instrumenter.end_event
157
+ end
158
+
159
+ def set_label(key, value)
160
+ instrumenter.set_label(key, value)
161
+ end
162
+
163
+ def set_custom_context(context)
164
+ instrumenter.set_custom_context(context)
165
+ end
166
+
167
+ def set_user(user)
168
+ instrumenter.set_user(user)
169
+ end
170
+
171
+ def build_context(rack_env:, for_type:)
172
+ @context_builder.build(rack_env: rack_env, for_type: for_type)
173
+ end
174
+
175
+ # errors
176
+
177
+ def report(exception, context: nil, handled: true)
178
+ return unless config.recording?
179
+ detect_forking!
180
+ return if config.filter_exception_types.include?(exception.class.to_s)
181
+
182
+ error = @error_builder.build_exception(
183
+ exception,
184
+ context: context,
185
+ handled: handled
186
+ )
187
+ enqueue error
188
+ error.id
189
+ end
190
+
191
+ def report_message(message, context: nil, backtrace: nil, **attrs)
192
+ return unless config.recording?
193
+ detect_forking!
194
+
195
+ error = @error_builder.build_log(
196
+ message,
197
+ context: context,
198
+ backtrace: backtrace,
199
+ **attrs
200
+ )
201
+ enqueue error
202
+ error.id
203
+ end
204
+
205
+ # filters
206
+
207
+ def add_filter(key, callback)
208
+ transport.add_filter(key, callback)
209
+ end
210
+
211
+ # misc
212
+
213
+ def inspect
214
+ super.split.first + '>'
215
+ end
216
+
217
+ def detect_forking!
218
+ return if @pid == Process.pid
219
+
220
+ config.logger.debug "Detected forking,
221
+ restarting threads in process [PID:#{Process.pid}]"
222
+
223
+ central_config.handle_forking!
224
+ transport.handle_forking!
225
+ instrumenter.handle_forking!
226
+ metrics.handle_forking!
227
+
228
+ @pid = Process.pid
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,51 @@
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
+ class CentralConfig
22
+ # @api private
23
+ class CacheControl
24
+ def initialize(value)
25
+ @header = value
26
+ parse!(value)
27
+ end
28
+
29
+ attr_reader(
30
+ :must_revalidate,
31
+ :no_cache,
32
+ :no_store,
33
+ :no_transform,
34
+ :public,
35
+ :private,
36
+ :proxy_revalidate,
37
+ :max_age,
38
+ :s_maxage
39
+ )
40
+
41
+ private
42
+
43
+ def parse!(value)
44
+ value.split(',').each do |token|
45
+ k, v = token.split('=').map(&:strip)
46
+ instance_variable_set(:"@#{k.tr('-', '_')}", v ? v.to_i : true)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,184 @@
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/central_config/cache_control'
21
+
22
+ module Hoss
23
+ # @api private
24
+ class CentralConfig
25
+ include Logging
26
+
27
+ # @api private
28
+ class ResponseError < InternalError
29
+ def initialize(response)
30
+ @response = response
31
+ end
32
+
33
+ attr_reader :response
34
+ end
35
+ class ClientError < ResponseError; end
36
+ class ServerError < ResponseError; end
37
+
38
+ def initialize(config)
39
+ @config = config
40
+ @modified_options = {}
41
+ @authorization = "Bearer #{@config.api_key}"
42
+ @http = Transport::Connection::Http.new(config)
43
+ @etag = 1
44
+ end
45
+
46
+ attr_reader :config
47
+ attr_reader :scheduled_task, :promise # for specs
48
+
49
+ def start
50
+ return unless config.central_config?
51
+
52
+ debug 'Starting CentralConfig'
53
+
54
+ fetch_and_apply_config
55
+ end
56
+
57
+ def stop
58
+ debug 'Stopping CentralConfig'
59
+
60
+ @scheduled_task&.cancel
61
+ end
62
+
63
+ def fetch_and_apply_config
64
+ @promise =
65
+ Concurrent::Promise
66
+ .execute(&method(:fetch_config))
67
+ .on_success(&method(:handle_success))
68
+ .rescue(&method(:handle_error))
69
+ end
70
+
71
+ def fetch_config
72
+ resp = perform_request
73
+ case resp.status
74
+ when 200..299
75
+ resp
76
+ when 300..399
77
+ resp
78
+ when 400..499
79
+ resp
80
+ # raise ClientError, resp
81
+ when 500..599
82
+ resp
83
+ # raise ServerError, resp
84
+ end
85
+ end
86
+
87
+ def assign(update)
88
+ # For each updated option, store the original value,
89
+ # unless already stored
90
+ update.each_key do |key|
91
+ @modified_options[key] ||= config.get(key.to_sym)&.value
92
+ end
93
+
94
+ # If the new update doesn't set a previously modified option,
95
+ # revert it to the original
96
+ @modified_options.each_key do |key|
97
+ next if update.key?(key)
98
+ update[key] = @modified_options.delete(key)
99
+ end
100
+ @config.replace_options(update)
101
+ end
102
+
103
+ def handle_forking!
104
+ stop
105
+ start
106
+ end
107
+
108
+ private
109
+
110
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
111
+ def handle_success(resp)
112
+ if (etag = resp.headers['Etag'])
113
+ @etag = etag
114
+ end
115
+
116
+ if resp.status == 304
117
+ info 'Received 304 Not Modified'
118
+ else
119
+ if resp.body && !resp.body.empty?
120
+ update = JSON.parse(resp.body.to_s)
121
+ assign(update['data']) unless update.nil? or update['data'].nil?
122
+ end
123
+
124
+ if update && update.any?
125
+ info 'Updated config'
126
+ debug 'Modified: %s', update.inspect
127
+ debug 'Modified original options: %s', @modified_options.inspect
128
+ end
129
+ end
130
+
131
+ schedule_next_fetch(resp)
132
+
133
+ true
134
+ rescue Exception => e
135
+ error 'Failed to apply remote config, %s', e.inspect
136
+ debug e.backtrace.join('\n')
137
+ end
138
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
139
+
140
+ def handle_error(error)
141
+ puts error.backtrace
142
+ # For tests, WebMock failures don't have real responses
143
+ response = error.response if error.respond_to?(:response)
144
+
145
+ debug(
146
+ 'Failed fetching config: %s, trying again in %d seconds',
147
+ response&.body, @config.remote_config_fetch_interval
148
+ )
149
+
150
+ assign({})
151
+
152
+ schedule_next_fetch(response)
153
+ end
154
+
155
+ def perform_request
156
+ body = '{"query":"query AgentConfig {\n agentConfig {\n accountApiConfiguration {\n uuid\n hostBlacklist\n sanitizedHeaders\n sanitizedQueryParams\n sanitizedBodyFields {\n type\n value\n }\n bodyCapture\n }\n apis {\n uuid\n name\n rootDomain\n hosts\n configuration(mergeWithAccountConfiguration: true) {\n uuid\n sanitizedHeaders\n sanitizedQueryParams\n bodyCapture\n sanitizedBodyFields {\n type\n value\n }\n }\n }\n accountRestrictions {\n ingressDisabled\n }\n }\n}","operationName":"AgentConfig"}'
157
+ @http.post(api_host, body: body, headers: headers)
158
+ end
159
+
160
+ def api_host
161
+ @api_host ||=
162
+ config.api_host +
163
+ '/api/graphql'
164
+ end
165
+
166
+ def headers
167
+ { 'Etag': @etag, 'Authorization': @authorization, 'HOSS-SKIP-INSTRUMENTATION': true }
168
+ end
169
+
170
+ def schedule_next_fetch(resp = nil)
171
+ headers = resp&.headers
172
+ seconds =
173
+ if headers && headers['Cache-Control']
174
+ CacheControl.new(headers['Cache-Control']).max_age
175
+ else
176
+ @config.remote_config_fetch_interval
177
+ end
178
+
179
+ @scheduled_task =
180
+ Concurrent::ScheduledTask
181
+ .execute(seconds, &method(:fetch_and_apply_config))
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,64 @@
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 ChildDurations
23
+ # @api private
24
+ module Methods
25
+ def child_durations
26
+ @child_durations ||= Durations.new
27
+ end
28
+
29
+ def child_started
30
+ child_durations.start
31
+ end
32
+
33
+ def child_stopped
34
+ child_durations.stop
35
+ end
36
+ end
37
+
38
+ # @api private
39
+ class Durations
40
+ def initialize
41
+ @nesting_level = 0
42
+ @start = nil
43
+ @duration = 0
44
+ @mutex = Mutex.new
45
+ end
46
+
47
+ attr_reader :duration
48
+
49
+ def start
50
+ @mutex.synchronize do
51
+ @nesting_level += 1
52
+ @start = Util.micros if @nesting_level == 1
53
+ end
54
+ end
55
+
56
+ def stop
57
+ @mutex.synchronize do
58
+ @nesting_level -= 1
59
+ @duration = (Util.micros - @start) if @nesting_level == 0
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,42 @@
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
+ class Config
22
+ # @api private
23
+ class Bytes
24
+ MULTIPLIERS = {
25
+ 'kb' => 1024,
26
+ 'mb' => 1024 * 1_000,
27
+ 'gb' => 1024 * 100_000
28
+ }.freeze
29
+ REGEX = /^(\d+)(b|kb|mb|gb)?$/i.freeze
30
+
31
+ def initialize(default_unit: 'kb')
32
+ @default_unit = default_unit
33
+ end
34
+
35
+ def call(value)
36
+ _, amount, unit = REGEX.match(String(value)).to_a
37
+ unit ||= @default_unit
38
+ MULTIPLIERS.fetch(unit.downcase, 1) * amount.to_i
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
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
+ class Config
22
+ # @api private
23
+ class Duration
24
+ MULTIPLIERS = { 'ms' => 0.001, 'm' => 60 }.freeze
25
+ REGEX = /^(-)?(\d+)(m|ms|s)?$/i.freeze
26
+
27
+ def initialize(default_unit: 's')
28
+ @default_unit = default_unit
29
+ end
30
+
31
+ def call(str)
32
+ _, negative, amount, unit = REGEX.match(String(str)).to_a
33
+ unit ||= @default_unit
34
+ seconds = MULTIPLIERS.fetch(unit.downcase, 1) * amount.to_i
35
+ seconds = 0 - seconds if negative
36
+ seconds
37
+ end
38
+ end
39
+ end
40
+ end