spartan_apm 0.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +55 -0
- data/VERSION +1 -0
- data/app/assets/flatpickr-4.6.9/LICENSE.md +21 -0
- data/app/assets/flatpickr-4.6.9/flatpickr.min.css +13 -0
- data/app/assets/flatpickr-4.6.9/flatpickr.min.js +2 -0
- data/app/assets/nice-select2-2.0.0/LICENSE +21 -0
- data/app/assets/nice-select2-2.0.0/nice-select2.min.css +1 -0
- data/app/assets/nice-select2-2.0.0/nice-select2.min.js +1 -0
- data/app/assets/spartan.svg +5 -0
- data/app/views/_help.html.erb +147 -0
- data/app/views/index.html.erb +231 -0
- data/app/views/scripts.js +911 -0
- data/app/views/styles.css +332 -0
- data/config.ru +36 -0
- data/lib/spartan_apm/engine.rb +45 -0
- data/lib/spartan_apm/error_info.rb +17 -0
- data/lib/spartan_apm/instrumentation/active_record.rb +13 -0
- data/lib/spartan_apm/instrumentation/base.rb +36 -0
- data/lib/spartan_apm/instrumentation/bunny.rb +24 -0
- data/lib/spartan_apm/instrumentation/cassandra.rb +13 -0
- data/lib/spartan_apm/instrumentation/curb.rb +13 -0
- data/lib/spartan_apm/instrumentation/dalli.rb +13 -0
- data/lib/spartan_apm/instrumentation/elasticsearch.rb +18 -0
- data/lib/spartan_apm/instrumentation/excon.rb +13 -0
- data/lib/spartan_apm/instrumentation/http.rb +13 -0
- data/lib/spartan_apm/instrumentation/httpclient.rb +13 -0
- data/lib/spartan_apm/instrumentation/net_http.rb +13 -0
- data/lib/spartan_apm/instrumentation/redis.rb +13 -0
- data/lib/spartan_apm/instrumentation/typhoeus.rb +13 -0
- data/lib/spartan_apm/instrumentation.rb +71 -0
- data/lib/spartan_apm/measure.rb +172 -0
- data/lib/spartan_apm/metric.rb +26 -0
- data/lib/spartan_apm/middleware/rack/end_middleware.rb +29 -0
- data/lib/spartan_apm/middleware/rack/start_middleware.rb +57 -0
- data/lib/spartan_apm/middleware/sidekiq/end_middleware.rb +25 -0
- data/lib/spartan_apm/middleware/sidekiq/start_middleware.rb +34 -0
- data/lib/spartan_apm/middleware.rb +16 -0
- data/lib/spartan_apm/persistence.rb +648 -0
- data/lib/spartan_apm/report.rb +436 -0
- data/lib/spartan_apm/string_cache.rb +27 -0
- data/lib/spartan_apm/web/api_request.rb +133 -0
- data/lib/spartan_apm/web/helpers.rb +88 -0
- data/lib/spartan_apm/web/router.rb +90 -0
- data/lib/spartan_apm/web.rb +10 -0
- data/lib/spartan_apm.rb +399 -0
- data/spartan_apm.gemspec +39 -0
- metadata +161 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
module SpartanAPM
|
6
|
+
module Web
|
7
|
+
# Rack application for serving up the web UI. The application can either
|
8
|
+
# be mounted as a Rack application or as Rack middleware.
|
9
|
+
class Router
|
10
|
+
ASSET_ROOT_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "app", "assets"))
|
11
|
+
|
12
|
+
def initialize(app = nil, root_path = "")
|
13
|
+
@app = app
|
14
|
+
@root_path = root_path.chomp("/")
|
15
|
+
@root_prefix = "#{@root_path}/"
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
path = env["PATH_INFO"]
|
20
|
+
response = nil
|
21
|
+
if path.start_with?(@root_path)
|
22
|
+
app_path = path[@root_path.length, path.length]
|
23
|
+
request = Rack::Request.new(env)
|
24
|
+
case app_path
|
25
|
+
when ""
|
26
|
+
response = [302, {"Location" => root_url(request)}, []]
|
27
|
+
when "/"
|
28
|
+
body = Helpers.new(request).render("index.html.erb")
|
29
|
+
response = [200, {"Content-Type" => "text/html; charset=utf-8"}, [body]]
|
30
|
+
when "/metrics"
|
31
|
+
response = json_response(ApiRequest.new(request).metrics)
|
32
|
+
when "/live_metrics"
|
33
|
+
response = json_response(ApiRequest.new(request).live_metrics)
|
34
|
+
when "/errors"
|
35
|
+
response = json_response(ApiRequest.new(request).errors)
|
36
|
+
when "/actions"
|
37
|
+
response = json_response(ApiRequest.new(request).actions)
|
38
|
+
else
|
39
|
+
if app_path.start_with?("/assets/")
|
40
|
+
response = asset_response(app_path.sub("/assets/", ""))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if response
|
46
|
+
response
|
47
|
+
elsif @app.nil?
|
48
|
+
not_found_response
|
49
|
+
else
|
50
|
+
@app.call(env)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def not_found_response
|
57
|
+
[404, {"Content-Type" => "text/plain"}, ["Not found"]]
|
58
|
+
end
|
59
|
+
|
60
|
+
def json_response(result)
|
61
|
+
[200, {"Content-Type" => "application/json; charset=utf-8"}, [JSON.dump(result)]]
|
62
|
+
end
|
63
|
+
|
64
|
+
def asset_response(asset_path)
|
65
|
+
file_path = File.expand_path(File.join(ASSET_ROOT_DIR, asset_path.split("/")))
|
66
|
+
return nil unless file_path.start_with?(ASSET_ROOT_DIR) && File.exist?(file_path)
|
67
|
+
data = File.read(file_path)
|
68
|
+
headers = {"Content-Type" => mime_type(file_path)}
|
69
|
+
headers["Cache-Control"] = "max-age=604800" if asset_path.match?(/\d\./)
|
70
|
+
[200, headers, [data]]
|
71
|
+
end
|
72
|
+
|
73
|
+
def mime_type(file_path)
|
74
|
+
extension = file_path.split(".").last
|
75
|
+
case extension
|
76
|
+
when "css"
|
77
|
+
"text/css"
|
78
|
+
when "js"
|
79
|
+
"application/javascript"
|
80
|
+
else
|
81
|
+
"application/octet-stream"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def root_url(request)
|
86
|
+
request.url.sub(/$|(\?.*)/, '/\1')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/spartan_apm.rb
ADDED
@@ -0,0 +1,399 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "digest"
|
5
|
+
require "zlib"
|
6
|
+
require "time"
|
7
|
+
require "json"
|
8
|
+
|
9
|
+
require "redis"
|
10
|
+
require "msgpack"
|
11
|
+
require "concurrent-ruby"
|
12
|
+
require "rack"
|
13
|
+
|
14
|
+
module SpartanAPM
|
15
|
+
DEFAULT_TTL = 60 * 60 * 24 * 7
|
16
|
+
|
17
|
+
# These are the default locations of javascript and CSS assets.
|
18
|
+
@plotly_js_url = ENV.fetch("SPARTAN_APM_PLOTLY_JS_URL", "https://cdn.plot.ly/plotly-basic-2.9.0.min.js")
|
19
|
+
|
20
|
+
@ignore_patterns = {}
|
21
|
+
|
22
|
+
@max_actions = 100
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# The configure method is just syntactic sugar to call a block that yields this module.
|
26
|
+
# ```
|
27
|
+
# SpartanAPM.configure do |config|
|
28
|
+
# config.env = "production"
|
29
|
+
# config.sample_rate = 0.1
|
30
|
+
# end
|
31
|
+
# ```
|
32
|
+
def configure
|
33
|
+
yield self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the environment name for the application. This can be used if you want to share
|
37
|
+
# the same Redis server between different environments. For instance if you have multiple
|
38
|
+
# staging environments, you could have them all share a single Redis rather than have to
|
39
|
+
# stand up a dedicated one for each environment. The value can also be set with the
|
40
|
+
# SPARTAN_APM_ENV environment variable.
|
41
|
+
def env=(value)
|
42
|
+
value = value.to_s.dup.freeze
|
43
|
+
raise ArgumentError.new("env cannot contain a tilda") if value.include?("~")
|
44
|
+
@_env_set = true
|
45
|
+
@env = value
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String] The environment name for the application.
|
49
|
+
def env
|
50
|
+
@env ||= ENV.fetch("SPARTAN_APM_ENV", "default")
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def env_set?
|
55
|
+
!!(defined?(@_env_set) && @_env_set)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set the list of available environments for the Web UI. This is only useful if there
|
59
|
+
# are multiple environments being saved to the same Redis database.
|
60
|
+
# @param value [Array<String>] List of environments.
|
61
|
+
def environments=(value)
|
62
|
+
@environments = Array(value).collect(&:to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the list of environments being tracked. This is used in the web UI to allow switching
|
66
|
+
# between multiple environments that are being stored to the same Redis database.
|
67
|
+
# @return [Array<String>] List of environments.
|
68
|
+
def environments
|
69
|
+
@environments ||= ENV.fetch("SPARTAN_APM_ENVIRONMENTS", env).split(/\s*,\s*/)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the sample rate for monitoring. If your application gets a lot traffic, you
|
73
|
+
# can use this setting to only record a percentage of it rather than every single
|
74
|
+
# request. The value can also be set with the SPARTAN_APM_SAMPLE_RATE environment
|
75
|
+
# variable
|
76
|
+
def sample_rate=(value)
|
77
|
+
@sample_rate = value&.to_f
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Float] The sample rate for the application.
|
81
|
+
def sample_rate
|
82
|
+
@sample_rate ||= ENV.fetch("SPARTAN_APM_SAMPLE_RATE", "1.0").to_f
|
83
|
+
end
|
84
|
+
|
85
|
+
# Set the time to live in seconds for how long data will be stored in Redis.
|
86
|
+
# The value can also be set with the SPARTAN_APM_TTL environment variable.
|
87
|
+
def ttl=(value)
|
88
|
+
value = value.to_i
|
89
|
+
raise ArgumentError.new("ttl must be > 0") if value < 0
|
90
|
+
@ttl = value
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [Integer] The time to live in seconds for how long data will be persisted in Redis.
|
94
|
+
def ttl
|
95
|
+
unless defined?(@ttl)
|
96
|
+
value = ENV["SPARTAN_APM_TTL"].to_i
|
97
|
+
value = DEFAULT_TTL if value <= 0
|
98
|
+
@ttl = value
|
99
|
+
end
|
100
|
+
@ttl
|
101
|
+
end
|
102
|
+
|
103
|
+
# Maximum number of actions that will be tracked. If more actions than this are being
|
104
|
+
# used by the application, then only the top most used actions by request time will
|
105
|
+
# be tracked.
|
106
|
+
attr_accessor :max_actions
|
107
|
+
|
108
|
+
# Set a Logger object where events can be logged. You can set this if you want
|
109
|
+
# to record how long it takes to persist the instrumentation data to Redis.
|
110
|
+
attr_writer :logger
|
111
|
+
|
112
|
+
# @return [Logger]
|
113
|
+
def logger
|
114
|
+
@logger = nil unless defined?(@logger)
|
115
|
+
@logger
|
116
|
+
end
|
117
|
+
|
118
|
+
# Set an optional application name to show in the web UI.
|
119
|
+
attr_writer :application_name
|
120
|
+
|
121
|
+
def application_name
|
122
|
+
@application_name ||= ENV.fetch("SPARTAN_APM_APPLICATION_NAME", "SpartanAPM")
|
123
|
+
end
|
124
|
+
|
125
|
+
# Set an option URL for a link in the web UI header.
|
126
|
+
attr_accessor :application_url
|
127
|
+
|
128
|
+
# URL for authenticating access to the application. This would normally be some kind of
|
129
|
+
# login page. Browsers will be redirected here if they are denied access to the web UI.
|
130
|
+
attr_accessor :authentication_url
|
131
|
+
|
132
|
+
# Set the list of app names to show in the web UI.
|
133
|
+
def apps=(value)
|
134
|
+
@apps = Array(value).collect(&:to_s)
|
135
|
+
end
|
136
|
+
|
137
|
+
def apps
|
138
|
+
@apps ||= ENV.fetch("SPARTAN_APM_APPS", "").split(/\s*,\s*/)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Set a list of errors to ignore. If an error matches a designated class,
|
142
|
+
# then it will not be recorded as an error. You can use this to strip out
|
143
|
+
# expected errors so they don't show up in the error monitoring.
|
144
|
+
# @param error_classes [Class, Module, String] class, class name, or module to ignore
|
145
|
+
def ignore_errors(error_classes)
|
146
|
+
error_classes = Array(error_classes)
|
147
|
+
@ignore_error_modules = error_classes.select { |klass| klass.is_a?(Module) }
|
148
|
+
@ignore_error_class_names = Set.new(error_classes.reject { |klass| klass.is_a?(Module) })
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Boolean] Returns true if the error matches an error class specified with ignore_errors.
|
152
|
+
def ignore_error?(error)
|
153
|
+
return false unless defined?(@ignore_error_class_names) && @ignore_error_class_names
|
154
|
+
return true if @ignore_error_modules.any? { |klass| error.is_a?(klass) }
|
155
|
+
return false if @ignore_error_class_names.empty?
|
156
|
+
ignore_error = false
|
157
|
+
klass = error.class
|
158
|
+
while klass && klass.superclass != klass
|
159
|
+
if @ignore_error_class_names.include?(klass.name)
|
160
|
+
ignore_error = true
|
161
|
+
break
|
162
|
+
end
|
163
|
+
klass = klass.superclass
|
164
|
+
end
|
165
|
+
ignore_error
|
166
|
+
end
|
167
|
+
|
168
|
+
# A backtrace cleaner can be set with a Proc (or anything that responds to `call`) to
|
169
|
+
# remove extraneous lines from error backtraces. Setting up a backtrace cleaner
|
170
|
+
# can save space in redis and make the traces easier to follow by removing uninteresting
|
171
|
+
# lines. The Proc will take a backtrace as an array and should return an array.
|
172
|
+
attr_writer :backtrace_cleaner
|
173
|
+
|
174
|
+
# Clean an error backtrace by passing it to the backtrace cleaner if one was set.
|
175
|
+
def clean_backtrace(backtrace)
|
176
|
+
if defined?(@backtrace_cleaner) && @backtrace_cleaner && backtrace
|
177
|
+
cleaned = Array(@backtrace_cleaner.call(backtrace[1, backtrace.size]))
|
178
|
+
# Alway make sure the top line of the trace is included.
|
179
|
+
cleaned.unshift(backtrace[0])
|
180
|
+
cleaned
|
181
|
+
else
|
182
|
+
backtrace
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# The measure block should be wrapped around whatever you want to consider a request
|
187
|
+
# in your application (i.e. web request, asynchronous job execution, etc.). This method
|
188
|
+
# will collect all instrumentation stats that occur within the block and report them
|
189
|
+
# as a single unit.
|
190
|
+
#
|
191
|
+
# In order to measure both an app name and an action name need to be provided. The app
|
192
|
+
# name should be something that describes the application function (i.e. "web" for web
|
193
|
+
# requests) while the action should describe the particular action being taken by the
|
194
|
+
# request (i.e. controller and action for a web request, worker name for an asynchronous
|
195
|
+
# job, etc.).
|
196
|
+
#
|
197
|
+
# The action name is optional here, but it is required in order for metrics to
|
198
|
+
# be recorded. This is because a measure block should encompass as much of the request
|
199
|
+
# as possible in order to be accurate. However, this might be before the action name
|
200
|
+
# is known. The action can be provided retroactively from within the block by setting
|
201
|
+
# `SpartanAPM.current_action`.
|
202
|
+
#
|
203
|
+
# @param app [String, Symbol] The name of the app that is collecting metrics.
|
204
|
+
# @param action [String, Symbol] The name of the action that is being performed.
|
205
|
+
def measure(app, action = nil)
|
206
|
+
if Thread.current[:spartan_apm_measure].nil? && (sample_rate >= 1.0 || rand < sample_rate)
|
207
|
+
measure = Measure.new(app, action)
|
208
|
+
Thread.current[:spartan_apm_measure] = measure
|
209
|
+
# rubocop:disable Lint/RescueException
|
210
|
+
begin
|
211
|
+
yield
|
212
|
+
rescue Exception => e
|
213
|
+
measure.capture_error(e) unless ignore_error?(e)
|
214
|
+
raise
|
215
|
+
ensure
|
216
|
+
Thread.current[:spartan_apm_measure] = nil
|
217
|
+
measure.record!
|
218
|
+
end
|
219
|
+
# rubocop:enable Lint/RescueException
|
220
|
+
else
|
221
|
+
yield
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Set the action name for the current measure. This allows you to set up a measure
|
226
|
+
# before you know the action name and then provide it later once that information
|
227
|
+
# is available.
|
228
|
+
# @param action [String, Symbol]
|
229
|
+
def current_action=(action)
|
230
|
+
measure = Thread.current[:spartan_apm_measure]
|
231
|
+
measure.action = action if measure
|
232
|
+
end
|
233
|
+
|
234
|
+
# Set the app name for the current measure. This allows you to set up a measure
|
235
|
+
# and override the app name if you want to segment some of your requests into a
|
236
|
+
# different app.
|
237
|
+
# @param app [String, Symbol]
|
238
|
+
def current_app=(app)
|
239
|
+
measure = Thread.current[:spartan_apm_measure]
|
240
|
+
measure.app = app if measure
|
241
|
+
end
|
242
|
+
|
243
|
+
# Capture a metric for a block. The metric value recorded will be the amount of time
|
244
|
+
# in seconds elapsed during block execution and will be stored with the current
|
245
|
+
# measure.
|
246
|
+
#
|
247
|
+
# The exclusive flag can be set to true to indicate that no other components should
|
248
|
+
# be captured in the block. This can be used if you want to measure one component
|
249
|
+
# as a unit but that component calls other instrumented components. For example, if you
|
250
|
+
# are instrumenting HTTP requests, but also want to instrument a service call that makes
|
251
|
+
# an HTTP request, you would capture the service call as exclusive.
|
252
|
+
#
|
253
|
+
# @param name [String, Symbol] The name of the component being measured.
|
254
|
+
# @param exclusive [Boolean]
|
255
|
+
def capture(name, exclusive: false)
|
256
|
+
measure = Thread.current[:spartan_apm_measure]
|
257
|
+
if measure
|
258
|
+
measure.capture(name, exclusive: exclusive) { yield }
|
259
|
+
else
|
260
|
+
yield
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Capture a time for a component in the current measure. This is the same as calling
|
265
|
+
# `capture` but without a block and passing the value explicitly.
|
266
|
+
def capture_time(name, elapsed_time)
|
267
|
+
measure = Thread.current[:spartan_apm_measure]
|
268
|
+
measure&.capture_time(name, elapsed_time)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Capture an error in the current measure.
|
272
|
+
def capture_error(error)
|
273
|
+
measure = Thread.current[:spartan_apm_measure]
|
274
|
+
if measure && !ignore_error?(error)
|
275
|
+
measure.capture_error(error)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Set the host name for the current process. Defaults to the system host name.
|
280
|
+
attr_writer :host
|
281
|
+
|
282
|
+
# @return The host name.
|
283
|
+
def host
|
284
|
+
@host ||= Socket.gethostname
|
285
|
+
end
|
286
|
+
|
287
|
+
# Set the Redis instance to use for storing metrics. This can be either a Redis
|
288
|
+
# instance or a Proc that returns a Redis instance. If it is a Proc, the Proc
|
289
|
+
# will be be called at runtime.
|
290
|
+
#
|
291
|
+
# If the Redis instance is not explicitly set, then the default connection URL
|
292
|
+
# will be looked up from environment variables. If the value of `SPARTAN_APM_REDIS_URL_PROVIDER`
|
293
|
+
# is set, then the URL will be gotten from the named environment variable. Otherwise
|
294
|
+
# it will use the value in `REDIS_URL`.
|
295
|
+
attr_writer :redis
|
296
|
+
|
297
|
+
# @return [Redis] Redis instance where metrics are stored.
|
298
|
+
def redis
|
299
|
+
@redis ||= default_redis
|
300
|
+
@redis.is_a?(Proc) ? @redis.call : @redis
|
301
|
+
end
|
302
|
+
|
303
|
+
# Set a list of patterns to ignore for an app. Patterns can be passed in as either
|
304
|
+
# a string to match or a Regexp. Strings can be specified with wildcards using "*"
|
305
|
+
# (i.e. "/debug/*" would match "/debug/info", etc.).
|
306
|
+
# @param app [String, Symbol] The app name the patterns are for.
|
307
|
+
# @param patterns [String, Regexp] Patterns to match that should be ignored.
|
308
|
+
def ignore_requests(app, *patterns)
|
309
|
+
app = app.to_s
|
310
|
+
patterns = patterns.compact
|
311
|
+
if patterns.empty?
|
312
|
+
@ignore_patterns.delete(app)
|
313
|
+
else
|
314
|
+
regexp_patterns = patterns.flatten.collect do |pattern|
|
315
|
+
if pattern.is_a?(Regexp)
|
316
|
+
pattern
|
317
|
+
else
|
318
|
+
exp = Regexp.escape(pattern).gsub("\\*", "(?:.*)")
|
319
|
+
Regexp.new("\\A#{exp}\\z")
|
320
|
+
end
|
321
|
+
end
|
322
|
+
@ignore_patterns[app] = Regexp.union(regexp_patterns)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Determine if a value is set to be ignored. This method should be called
|
327
|
+
# by whatever code is calling `measure`. There is no set definition for what the
|
328
|
+
# value being passed should be and it left up to the implementation to set
|
329
|
+
# a convention.
|
330
|
+
#
|
331
|
+
# For instance in the bundled Rack implementation, the value passed to this
|
332
|
+
# method is the web request path. This allows that implementation to disable
|
333
|
+
# instrumentation for a set of request paths.
|
334
|
+
def ignore_request?(app, value)
|
335
|
+
return false if value.nil?
|
336
|
+
!!@ignore_patterns[app.to_s]&.match(value.to_s)
|
337
|
+
end
|
338
|
+
|
339
|
+
# The web UI will use a locked version of the plotly.js library from the official
|
340
|
+
# distribution CDN. If your company security policy requires all tools to pull
|
341
|
+
# from an internal source, you can change the URL with this setting.
|
342
|
+
attr_accessor :plotly_js_url
|
343
|
+
|
344
|
+
# @api private
|
345
|
+
# Get the bucket for the specified time.
|
346
|
+
# @return [Integer]
|
347
|
+
def bucket(time)
|
348
|
+
(time.to_f / 60.0).floor
|
349
|
+
end
|
350
|
+
|
351
|
+
# @api private
|
352
|
+
# Get the time for the specified bucket.
|
353
|
+
# @return [Time]
|
354
|
+
def bucket_time(bucket)
|
355
|
+
Time.at(bucket * 60.0)
|
356
|
+
end
|
357
|
+
|
358
|
+
# @api private
|
359
|
+
# Used for testing for disabling asynchronous metric persistence.
|
360
|
+
def persist_asynchronously?
|
361
|
+
unless defined?(@persist_asynchronously)
|
362
|
+
@persist_asynchronously = true
|
363
|
+
end
|
364
|
+
@persist_asynchronously
|
365
|
+
end
|
366
|
+
|
367
|
+
# @api private
|
368
|
+
# Used for testing for disabling asynchronous metric persistence.
|
369
|
+
def persist_asynchronously=(value)
|
370
|
+
@persist_asynchronously = !!value
|
371
|
+
end
|
372
|
+
|
373
|
+
private
|
374
|
+
|
375
|
+
def default_redis
|
376
|
+
var_name = ENV.fetch("SPARTAN_APM_REDIS_URL_PROVIDER", "REDIS_URL")
|
377
|
+
url = ENV[var_name]
|
378
|
+
Redis.new(url: url)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
require_relative "spartan_apm/string_cache"
|
384
|
+
require_relative "spartan_apm/error_info"
|
385
|
+
require_relative "spartan_apm/measure"
|
386
|
+
require_relative "spartan_apm/persistence"
|
387
|
+
require_relative "spartan_apm/report"
|
388
|
+
require_relative "spartan_apm/metric"
|
389
|
+
require_relative "spartan_apm/instrumentation"
|
390
|
+
require_relative "spartan_apm/middleware"
|
391
|
+
require_relative "spartan_apm/web"
|
392
|
+
|
393
|
+
if defined?(Rails::Engine)
|
394
|
+
require_relative "spartan_apm/engine"
|
395
|
+
end
|
396
|
+
|
397
|
+
at_exit do
|
398
|
+
SpartanAPM::Measure.flush
|
399
|
+
end
|
data/spartan_apm.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "spartan_apm"
|
3
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
|
4
|
+
spec.authors = ["Brian Durand"]
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "Simple redis backed application performance monitoring tool."
|
8
|
+
spec.homepage = "https://github.com/bdurand/spartan_apm"
|
9
|
+
spec.license = "MIT"
|
10
|
+
|
11
|
+
# Specify which files should be added to the gem when it is released.
|
12
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
13
|
+
ignore_files = %w[
|
14
|
+
.
|
15
|
+
Appraisals
|
16
|
+
Gemfile
|
17
|
+
Gemfile.lock
|
18
|
+
Rakefile
|
19
|
+
bin/
|
20
|
+
docker/
|
21
|
+
gemfiles/
|
22
|
+
sample_data
|
23
|
+
spec/
|
24
|
+
]
|
25
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
27
|
+
end
|
28
|
+
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_dependency "redis"
|
32
|
+
spec.add_dependency "msgpack"
|
33
|
+
spec.add_dependency "concurrent-ruby"
|
34
|
+
spec.add_dependency "rack"
|
35
|
+
|
36
|
+
spec.add_development_dependency "bundler"
|
37
|
+
|
38
|
+
spec.required_ruby_version = ">= 2.5"
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spartan_apm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0.rc1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: msgpack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: concurrent-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- bbdurand@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- CHANGELOG.md
|
91
|
+
- MIT-LICENSE
|
92
|
+
- README.md
|
93
|
+
- VERSION
|
94
|
+
- app/assets/flatpickr-4.6.9/LICENSE.md
|
95
|
+
- app/assets/flatpickr-4.6.9/flatpickr.min.css
|
96
|
+
- app/assets/flatpickr-4.6.9/flatpickr.min.js
|
97
|
+
- app/assets/nice-select2-2.0.0/LICENSE
|
98
|
+
- app/assets/nice-select2-2.0.0/nice-select2.min.css
|
99
|
+
- app/assets/nice-select2-2.0.0/nice-select2.min.js
|
100
|
+
- app/assets/spartan.svg
|
101
|
+
- app/views/_help.html.erb
|
102
|
+
- app/views/index.html.erb
|
103
|
+
- app/views/scripts.js
|
104
|
+
- app/views/styles.css
|
105
|
+
- config.ru
|
106
|
+
- lib/spartan_apm.rb
|
107
|
+
- lib/spartan_apm/engine.rb
|
108
|
+
- lib/spartan_apm/error_info.rb
|
109
|
+
- lib/spartan_apm/instrumentation.rb
|
110
|
+
- lib/spartan_apm/instrumentation/active_record.rb
|
111
|
+
- lib/spartan_apm/instrumentation/base.rb
|
112
|
+
- lib/spartan_apm/instrumentation/bunny.rb
|
113
|
+
- lib/spartan_apm/instrumentation/cassandra.rb
|
114
|
+
- lib/spartan_apm/instrumentation/curb.rb
|
115
|
+
- lib/spartan_apm/instrumentation/dalli.rb
|
116
|
+
- lib/spartan_apm/instrumentation/elasticsearch.rb
|
117
|
+
- lib/spartan_apm/instrumentation/excon.rb
|
118
|
+
- lib/spartan_apm/instrumentation/http.rb
|
119
|
+
- lib/spartan_apm/instrumentation/httpclient.rb
|
120
|
+
- lib/spartan_apm/instrumentation/net_http.rb
|
121
|
+
- lib/spartan_apm/instrumentation/redis.rb
|
122
|
+
- lib/spartan_apm/instrumentation/typhoeus.rb
|
123
|
+
- lib/spartan_apm/measure.rb
|
124
|
+
- lib/spartan_apm/metric.rb
|
125
|
+
- lib/spartan_apm/middleware.rb
|
126
|
+
- lib/spartan_apm/middleware/rack/end_middleware.rb
|
127
|
+
- lib/spartan_apm/middleware/rack/start_middleware.rb
|
128
|
+
- lib/spartan_apm/middleware/sidekiq/end_middleware.rb
|
129
|
+
- lib/spartan_apm/middleware/sidekiq/start_middleware.rb
|
130
|
+
- lib/spartan_apm/persistence.rb
|
131
|
+
- lib/spartan_apm/report.rb
|
132
|
+
- lib/spartan_apm/string_cache.rb
|
133
|
+
- lib/spartan_apm/web.rb
|
134
|
+
- lib/spartan_apm/web/api_request.rb
|
135
|
+
- lib/spartan_apm/web/helpers.rb
|
136
|
+
- lib/spartan_apm/web/router.rb
|
137
|
+
- spartan_apm.gemspec
|
138
|
+
homepage: https://github.com/bdurand/spartan_apm
|
139
|
+
licenses:
|
140
|
+
- MIT
|
141
|
+
metadata: {}
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '2.5'
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
|
+
requirements:
|
153
|
+
- - ">"
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: 1.3.1
|
156
|
+
requirements: []
|
157
|
+
rubygems_version: 3.1.6
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: Simple redis backed application performance monitoring tool.
|
161
|
+
test_files: []
|