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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/Bug_report.md +40 -0
- data/.github/ISSUE_TEMPLATE/Feature_request.md +17 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +60 -0
- data/.gitignore +27 -0
- data/.rspec +2 -0
- data/Dockerfile +43 -0
- data/Gemfile +105 -0
- data/LICENSE +201 -0
- data/hoss-agent.gemspec +42 -0
- data/lib/hoss/agent.rb +231 -0
- data/lib/hoss/central_config/cache_control.rb +51 -0
- data/lib/hoss/central_config.rb +184 -0
- data/lib/hoss/child_durations.rb +64 -0
- data/lib/hoss/config/bytes.rb +42 -0
- data/lib/hoss/config/duration.rb +40 -0
- data/lib/hoss/config/options.rb +154 -0
- data/lib/hoss/config/regexp_list.rb +30 -0
- data/lib/hoss/config/wildcard_pattern_list.rb +54 -0
- data/lib/hoss/config.rb +304 -0
- data/lib/hoss/context/request/socket.rb +36 -0
- data/lib/hoss/context/request/url.rb +59 -0
- data/lib/hoss/context/request.rb +28 -0
- data/lib/hoss/context/response.rb +47 -0
- data/lib/hoss/context/user.rb +59 -0
- data/lib/hoss/context.rb +64 -0
- data/lib/hoss/context_builder.rb +112 -0
- data/lib/hoss/deprecations.rb +39 -0
- data/lib/hoss/error/exception.rb +70 -0
- data/lib/hoss/error/log.rb +41 -0
- data/lib/hoss/error.rb +49 -0
- data/lib/hoss/error_builder.rb +90 -0
- data/lib/hoss/event.rb +131 -0
- data/lib/hoss/instrumenter.rb +107 -0
- data/lib/hoss/internal_error.rb +23 -0
- data/lib/hoss/logging.rb +70 -0
- data/lib/hoss/metadata/process_info.rb +35 -0
- data/lib/hoss/metadata/service_info.rb +76 -0
- data/lib/hoss/metadata/system_info/container_info.rb +136 -0
- data/lib/hoss/metadata/system_info.rb +47 -0
- data/lib/hoss/metadata.rb +36 -0
- data/lib/hoss/naively_hashable.rb +38 -0
- data/lib/hoss/rails.rb +68 -0
- data/lib/hoss/railtie.rb +42 -0
- data/lib/hoss/report.rb +9 -0
- data/lib/hoss/sinatra.rb +53 -0
- data/lib/hoss/spies/faraday.rb +102 -0
- data/lib/hoss/spies/http.rb +81 -0
- data/lib/hoss/spies/net_http.rb +97 -0
- data/lib/hoss/spies.rb +104 -0
- data/lib/hoss/stacktrace/frame.rb +66 -0
- data/lib/hoss/stacktrace.rb +33 -0
- data/lib/hoss/stacktrace_builder.rb +124 -0
- data/lib/hoss/transport/base.rb +191 -0
- data/lib/hoss/transport/connection/http.rb +139 -0
- data/lib/hoss/transport/connection/proxy_pipe.rb +94 -0
- data/lib/hoss/transport/connection.rb +55 -0
- data/lib/hoss/transport/filters/hash_sanitizer.rb +77 -0
- data/lib/hoss/transport/filters/secrets_filter.rb +48 -0
- data/lib/hoss/transport/filters.rb +60 -0
- data/lib/hoss/transport/headers.rb +74 -0
- data/lib/hoss/transport/serializers/context_serializer.rb +112 -0
- data/lib/hoss/transport/serializers/error_serializer.rb +92 -0
- data/lib/hoss/transport/serializers/event_serializer.rb +73 -0
- data/lib/hoss/transport/serializers/metadata_serializer.rb +92 -0
- data/lib/hoss/transport/serializers/report_serializer.rb +33 -0
- data/lib/hoss/transport/serializers.rb +113 -0
- data/lib/hoss/transport/user_agent.rb +48 -0
- data/lib/hoss/transport/worker.rb +319 -0
- data/lib/hoss/util/inflector.rb +110 -0
- data/lib/hoss/util/lru_cache.rb +65 -0
- data/lib/hoss/util/throttle.rb +52 -0
- data/lib/hoss/util.rb +54 -0
- data/lib/hoss/version.rb +22 -0
- data/lib/hoss-agent.rb +210 -0
- data/lib/hoss.rb +21 -0
- metadata +147 -0
@@ -0,0 +1,319 @@
|
|
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
|
+
debug 'working message', msg
|
65
|
+
|
66
|
+
# wait if we don't have a config
|
67
|
+
while config.agentConfig.nil?
|
68
|
+
sleep 0.1
|
69
|
+
end
|
70
|
+
|
71
|
+
case msg
|
72
|
+
when StopMessage
|
73
|
+
debug 'Stopping worker -- %s', self
|
74
|
+
done = true
|
75
|
+
break
|
76
|
+
else
|
77
|
+
batch = []
|
78
|
+
current_batch_size = 0
|
79
|
+
|
80
|
+
# Use this as the first message in the batch
|
81
|
+
event = msg.filtered ? msg : filter_resource(msg)
|
82
|
+
unless event.nil?
|
83
|
+
event_size = resource_size(event)
|
84
|
+
if current_batch_size + event_size <= @config.batch_size
|
85
|
+
unless host_blacklisted(event)
|
86
|
+
current_batch_size += event_size
|
87
|
+
if event.retries < @config.max_event_retries
|
88
|
+
batch.push(event)
|
89
|
+
else
|
90
|
+
debug "max retries hit for event"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
else
|
94
|
+
debug "Event is too large, body needs to be truncated"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Do inner loop reading queue to build report
|
99
|
+
requeue_messages = []
|
100
|
+
while current_batch_size < @config.batch_size && !queue.empty?
|
101
|
+
next_msg = queue.pop
|
102
|
+
case next_msg
|
103
|
+
when StopMessage
|
104
|
+
debug 'Stopping worker -- %s', self
|
105
|
+
done = true
|
106
|
+
break
|
107
|
+
else
|
108
|
+
event = next_msg.filtered ? next_msg : filter_resource(next_msg) unless
|
109
|
+
unless event.nil?
|
110
|
+
event_size = resource_size(event)
|
111
|
+
if current_batch_size + event_size <= @config.batch_size
|
112
|
+
unless host_blacklisted(event)
|
113
|
+
current_batch_size += event_size
|
114
|
+
if event.retries < @config.max_event_retries
|
115
|
+
batch.push(event)
|
116
|
+
else
|
117
|
+
debug "max retries hit for event"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
else
|
121
|
+
debug "Event too large for this batch, requeue"
|
122
|
+
requeue_messages.push(event)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
if batch.length == 0
|
129
|
+
debug "batch is empty, breaking"
|
130
|
+
break
|
131
|
+
end
|
132
|
+
|
133
|
+
debug "Requeue #{requeue_messages.length} messages" if requeue_messages.length > 0
|
134
|
+
requeue_messages.each {|msg| queue.push(msg, false) }
|
135
|
+
|
136
|
+
report = Report.new
|
137
|
+
report.events = batch.map {|event| serializers.serialize(event) }
|
138
|
+
|
139
|
+
debug "Finished building report"
|
140
|
+
data = serializers.serialize(report)
|
141
|
+
json = JSON.fast_generate(data)
|
142
|
+
begin
|
143
|
+
debug json
|
144
|
+
rescue Exception => e
|
145
|
+
debug 'unable to print body'
|
146
|
+
puts json
|
147
|
+
end
|
148
|
+
begin
|
149
|
+
connection.write(json)
|
150
|
+
rescue Exception => e
|
151
|
+
error format('Failed send report: %s %s', e.inspect, e.backtrace)
|
152
|
+
batch.each do |m|
|
153
|
+
m.retries += 1
|
154
|
+
queue.push(m, false)
|
155
|
+
end
|
156
|
+
sleep 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
rescue Exception => e
|
161
|
+
warn 'Worker died with exception: %s', e.inspect
|
162
|
+
debug e.backtrace.join("\n")
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def resource_size(resource)
|
168
|
+
size = 20
|
169
|
+
size += JSON.fast_generate(resource.request.headers).length if resource.request && resource.request.headers
|
170
|
+
size += JSON.fast_generate(resource.response.headers).length if resource.response && resource.response.headers
|
171
|
+
size += resource.request.body.length if resource.request && resource.request.body
|
172
|
+
size += resource.response.body.length if resource.response && resource.response.body
|
173
|
+
size
|
174
|
+
end
|
175
|
+
|
176
|
+
def decompress_body(body, encoding)
|
177
|
+
return body unless body
|
178
|
+
case (encoding || '').downcase
|
179
|
+
when "gzip"
|
180
|
+
Zlib::GzipReader.new(StringIO.new(body)).read.to_s
|
181
|
+
when "deflate"
|
182
|
+
Zlib::Inflate.new.inflate(body)
|
183
|
+
else
|
184
|
+
body
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_header(headers, name)
|
189
|
+
headers[headers.keys.select{|h| h.casecmp(name) == 0}[0]]
|
190
|
+
end
|
191
|
+
|
192
|
+
def filter_resource(resource)
|
193
|
+
# Make sure the body is not compressed
|
194
|
+
resource.request.body = decompress_body(resource.request.body, get_header(resource.request.headers, "content-encoding")) if resource.request
|
195
|
+
resource.response.body = decompress_body(resource.response.body, get_header(resource.response.headers,"content-encoding")) if resource.response
|
196
|
+
|
197
|
+
# Start applying filters
|
198
|
+
config = get_configuration_for_host(resource.request.headers['host'])
|
199
|
+
debug "Using config for event #{config.to_s}"
|
200
|
+
|
201
|
+
# Filter the headers
|
202
|
+
filter_headers(resource.request.headers, config['sanitizedHeaders']) if resource.request
|
203
|
+
filter_headers(resource.response.headers, config['sanitizedHeaders']) if resource.response
|
204
|
+
|
205
|
+
# Filter the body
|
206
|
+
if config['bodyCapture'] == 'Off' || (config['bodyCapture'] == 'OnError' && !contains_error(resource))
|
207
|
+
resource.request.body = nil if resource.request
|
208
|
+
resource.response.body = nil if resource.response
|
209
|
+
else
|
210
|
+
resource.request.body = filter_body(resource.request.body, config['sanitizedBodyFields']) if resource.request
|
211
|
+
resource.response.body = filter_body(resource.response.body, config['sanitizedBodyFields']) if resource.response
|
212
|
+
end
|
213
|
+
|
214
|
+
# Filter the query params
|
215
|
+
resource.request.url = filter_url(resource.request.url, config['sanitizedQueryParams'])
|
216
|
+
|
217
|
+
# Record this event as filtered in case of retry
|
218
|
+
resource.filtered = true
|
219
|
+
|
220
|
+
resource
|
221
|
+
end
|
222
|
+
|
223
|
+
def contains_error(resource)
|
224
|
+
if !resource.response.nil? && resource.response.status_code >= 400
|
225
|
+
return true
|
226
|
+
end
|
227
|
+
false
|
228
|
+
end
|
229
|
+
|
230
|
+
def filter_url(url, config)
|
231
|
+
uri = URI(url)
|
232
|
+
if uri.query
|
233
|
+
query = CGI::parse(uri.query)
|
234
|
+
query.keys.each{|k| query[k] = 'xxx' if contains_string(config, k)}
|
235
|
+
uri.query = URI.encode_www_form(query)
|
236
|
+
return uri.to_s
|
237
|
+
end
|
238
|
+
url
|
239
|
+
rescue Exception => e
|
240
|
+
error "Error filtering url", e
|
241
|
+
url
|
242
|
+
end
|
243
|
+
|
244
|
+
def filter_headers(headers, names)
|
245
|
+
headers.each { |h| headers[h[0]] = 'xxx' if contains_string(names, h[0]) }
|
246
|
+
end
|
247
|
+
|
248
|
+
def filter_body(body, fields)
|
249
|
+
return nil if body.nil?
|
250
|
+
json = JSON.parse(body)
|
251
|
+
JSON.fast_generate(mask_object_fields(json, fields))
|
252
|
+
rescue Exception => e
|
253
|
+
body
|
254
|
+
end
|
255
|
+
|
256
|
+
def mask_object_fields(obj, fields)
|
257
|
+
field_names = fields.map {|f| f['value'] }
|
258
|
+
case obj
|
259
|
+
when Hash
|
260
|
+
obj.keys.each do |k|
|
261
|
+
if contains_string(field_names, k)
|
262
|
+
obj[k] = 'xxx'
|
263
|
+
else
|
264
|
+
obj[k] = mask_object_fields(obj[k], fields)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
when Array
|
268
|
+
obj.each_index do |i|
|
269
|
+
obj[i] = mask_object_fields(obj[i], fields) if (obj[i].is_a?(Hash) || obj[i].is_a?(Array))
|
270
|
+
end
|
271
|
+
end
|
272
|
+
obj
|
273
|
+
end
|
274
|
+
|
275
|
+
# Get the configuration for a host, either use a defined api or the default account config
|
276
|
+
def get_configuration_for_host(host)
|
277
|
+
api_config = api_configurations.select{ |api| contains_string(api['hosts'], host)}[0]
|
278
|
+
return api_config['configuration'] if api_config
|
279
|
+
account_api_configuration
|
280
|
+
end
|
281
|
+
|
282
|
+
# Parse out the host blacklist
|
283
|
+
def get_account_host_blacklist
|
284
|
+
account_api_configuration['hostBlacklist']
|
285
|
+
end
|
286
|
+
|
287
|
+
# Do a case insensitive search for a string in array
|
288
|
+
def contains_string(arr, a)
|
289
|
+
!arr.select { |b| b.casecmp(a) == 0 }.empty?
|
290
|
+
end
|
291
|
+
|
292
|
+
# Return 'accountApiConfiguration' or empty object
|
293
|
+
def account_api_configuration
|
294
|
+
return {} if config.agentConfig.nil? or config.agentConfig['accountApiConfiguration'].nil?
|
295
|
+
config.agentConfig['accountApiConfiguration']
|
296
|
+
end
|
297
|
+
|
298
|
+
# Return the 'apis' or empty object
|
299
|
+
def api_configurations
|
300
|
+
return {} if config.agentConfig.nil? or config.agentConfig['apis'].nil?
|
301
|
+
config.agentConfig['apis']
|
302
|
+
end
|
303
|
+
|
304
|
+
def host_blacklisted(event)
|
305
|
+
blacklist = get_account_host_blacklist
|
306
|
+
return false if blacklist.nil?
|
307
|
+
is_blacklisted = false
|
308
|
+
unless event.request.nil? || event.request.url.nil?
|
309
|
+
url = URI(event.request.url)
|
310
|
+
blacklist.each do |host|
|
311
|
+
is_blacklisted = true if url.host == host
|
312
|
+
is_blacklisted = true if url.host.end_with? '.'+host
|
313
|
+
end
|
314
|
+
end
|
315
|
+
is_blacklisted
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
@@ -0,0 +1,110 @@
|
|
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
|
+
# rubocop:disable all
|
22
|
+
module Util
|
23
|
+
# From https://github.com/rails/rails/blob/v5.2.0/activesupport/lib/active_support/inflector/methods.rb#L254-L332
|
24
|
+
module Inflector
|
25
|
+
extend self
|
26
|
+
|
27
|
+
#
|
28
|
+
# Tries to find a constant with the name specified in the argument string.
|
29
|
+
#
|
30
|
+
# constantize('Module') # => Module
|
31
|
+
# constantize('Foo::Bar') # => Foo::Bar
|
32
|
+
#
|
33
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
34
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
35
|
+
# account:
|
36
|
+
#
|
37
|
+
# C = 'outside'
|
38
|
+
# module M
|
39
|
+
# C = 'inside'
|
40
|
+
# C # => 'inside'
|
41
|
+
# constantize('C') # => 'outside', same as ::C
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# NameError is raised when the name is not in CamelCase or the constant is
|
45
|
+
# unknown.
|
46
|
+
def constantize(camel_cased_word)
|
47
|
+
names = camel_cased_word.split("::".freeze)
|
48
|
+
|
49
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
50
|
+
Object.const_get(camel_cased_word) if names.empty?
|
51
|
+
|
52
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
53
|
+
names.shift if names.size > 1 && names.first.empty?
|
54
|
+
|
55
|
+
names.inject(Object) do |constant, name|
|
56
|
+
if constant == Object
|
57
|
+
constant.const_get(name)
|
58
|
+
else
|
59
|
+
candidate = constant.const_get(name)
|
60
|
+
next candidate if constant.const_defined?(name, false)
|
61
|
+
next candidate unless Object.const_defined?(name)
|
62
|
+
|
63
|
+
# Go down the ancestors to check if it is owned directly. The check
|
64
|
+
# stops when we reach Object or the end of ancestors tree.
|
65
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
66
|
+
break const if ancestor == Object
|
67
|
+
break ancestor if ancestor.const_defined?(name, false)
|
68
|
+
const
|
69
|
+
end
|
70
|
+
|
71
|
+
# owner is in Object, so raise
|
72
|
+
constant.const_get(name, false)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Tries to find a constant with the name specified in the argument string.
|
78
|
+
#
|
79
|
+
# safe_constantize('Module') # => Module
|
80
|
+
# safe_constantize('Foo::Bar') # => Foo::Bar
|
81
|
+
#
|
82
|
+
# The name is assumed to be the one of a top-level constant, no matter
|
83
|
+
# whether it starts with "::" or not. No lexical context is taken into
|
84
|
+
# account:
|
85
|
+
#
|
86
|
+
# C = 'outside'
|
87
|
+
# module M
|
88
|
+
# C = 'inside'
|
89
|
+
# C # => 'inside'
|
90
|
+
# safe_constantize('C') # => 'outside', same as ::C
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# +nil+ is returned when the name is not in CamelCase or the constant (or
|
94
|
+
# part of it) is unknown.
|
95
|
+
#
|
96
|
+
# safe_constantize('blargle') # => nil
|
97
|
+
# safe_constantize('UnknownModule') # => nil
|
98
|
+
# safe_constantize('UnknownModule::Foo::Bar') # => nil
|
99
|
+
def safe_constantize(camel_cased_word)
|
100
|
+
constantize(camel_cased_word)
|
101
|
+
rescue NameError => e
|
102
|
+
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
|
103
|
+
e.name.to_s == camel_cased_word.to_s)
|
104
|
+
rescue ArgumentError => e
|
105
|
+
raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match(e.message)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
# rubocop:enable all
|
110
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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 Util
|
22
|
+
# @api private
|
23
|
+
class LruCache
|
24
|
+
def initialize(max_size = 512, &block)
|
25
|
+
@max_size = max_size
|
26
|
+
@data = Hash.new(&block)
|
27
|
+
@mutex = Mutex.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](key)
|
31
|
+
@mutex.synchronize do
|
32
|
+
val = @data[key]
|
33
|
+
return unless val
|
34
|
+
add(key, val)
|
35
|
+
val
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def []=(key, val)
|
40
|
+
@mutex.synchronize do
|
41
|
+
add(key, val)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def length
|
46
|
+
@data.length
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_a
|
50
|
+
@data.to_a
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def add(key, val)
|
56
|
+
@data.delete(key)
|
57
|
+
@data[key] = val
|
58
|
+
|
59
|
+
return unless @data.length > @max_size
|
60
|
+
|
61
|
+
@data.delete(@data.first[0])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,52 @@
|
|
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 Util
|
22
|
+
# @api private
|
23
|
+
|
24
|
+
# Usage example:
|
25
|
+
# Throttle.new(5) { thing to only do once per 5 secs }
|
26
|
+
class Throttle
|
27
|
+
def initialize(buffer_secs, &block)
|
28
|
+
@buffer_secs = buffer_secs
|
29
|
+
@block = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def call
|
33
|
+
if @last_call && seconds_since_last_call < @buffer_secs
|
34
|
+
return @last_result
|
35
|
+
end
|
36
|
+
|
37
|
+
@last_call = now
|
38
|
+
@last_result = @block.call
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def now
|
44
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
45
|
+
end
|
46
|
+
|
47
|
+
def seconds_since_last_call
|
48
|
+
now - @last_call
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/hoss/util.rb
ADDED
@@ -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
|
data/lib/hoss/version.rb
ADDED
@@ -0,0 +1,22 @@
|
|
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
|
+
VERSION = '1.0.1'
|
22
|
+
end
|