rorvswild 1.5.12 → 1.5.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +45 -11
- data/lib/rorvswild/agent.rb +30 -10
- data/lib/rorvswild/metrics/cpu.rb +31 -0
- data/lib/rorvswild/metrics/memory.rb +57 -0
- data/lib/rorvswild/metrics/storage.rb +17 -0
- data/lib/rorvswild/metrics.rb +54 -0
- data/lib/rorvswild/plugin/net_http.rb +1 -4
- data/lib/rorvswild/plugin/redis.rb +1 -1
- data/lib/rorvswild/queue.rb +7 -1
- data/lib/rorvswild/version.rb +1 -1
- data/lib/rorvswild.rb +8 -0
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29d23fa806fe2d9547169a35795b4b313bf7510264aba4d0f8f3471ae7acf047
|
4
|
+
data.tar.gz: 681d88bef1fb76ede952387ef1d8ffbdf4bfa4cbf08f990f7964624401ff71a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e54dba0143bdb8e94677519698857c7ddc62f91ecb0a876ecc9e0e2fb402e4d11055a59f03cca1c2ca5a2e73b82cf5380574d9e3e4c73d1a66e21c7ea54cfe6c
|
7
|
+
data.tar.gz: 92d4cb2e15015929ccc609b6b81837fb1a78ea12ebe4538555e0a023266fc852001d1aa17d7e620b50dd6ac3f856cbf08bba1d3cb9e4f24f10333b1faa26064d
|
data/README.md
CHANGED
@@ -89,24 +89,22 @@ If you are using `Rack::Deflater` middleware you won't see the small button in t
|
|
89
89
|
*RoRvsWild.com* makes it easy to monitor requests, background jobs and errors in your production and staging environment.
|
90
90
|
It also comes with some extra options listed below.
|
91
91
|
|
92
|
-
#### Measure any code
|
92
|
+
#### Measure any section of code
|
93
93
|
|
94
|
-
|
94
|
+
RorVsWild measures a lot of events such as SQL queries. But it might not be enough for you. There is a solution to measure any section of code to help you find the most hidden bottlenecks.
|
95
95
|
|
96
96
|
```ruby
|
97
|
-
|
98
|
-
|
97
|
+
# Measure a code given as a string
|
98
|
+
RorVsWild.measure("bubble_sort(array)")
|
99
99
|
|
100
|
-
|
100
|
+
# Measure a code given as a block
|
101
|
+
RorVsWild.measure { bubble_sort(array) }
|
101
102
|
|
102
|
-
|
103
|
-
RorVsWild.
|
103
|
+
# Measure a code given as a block with an optional description
|
104
|
+
RorVsWild.measure("Optional description") { bubble_sort(array) }
|
104
105
|
```
|
105
106
|
|
106
|
-
|
107
|
-
|
108
|
-
Note that Calling `measure_code` or `measure_block` inside or a request or a job will add a section.
|
109
|
-
That is convenient to profile finely parts of your code.
|
107
|
+
For each custom measure, a section is added with the file name and line number where it has been called.
|
110
108
|
|
111
109
|
#### Send errors manually
|
112
110
|
|
@@ -136,6 +134,18 @@ RorVsWild.record_error(exception, {something: "important"})
|
|
136
134
|
RorVsWild.catch_error(something: "important") { 1 / 0 }
|
137
135
|
```
|
138
136
|
|
137
|
+
It is also possible to pre-fill this context data at the begining of each request or job :
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class ApplicationController < ActionController::Base
|
141
|
+
before_action :prefill_error_context
|
142
|
+
|
143
|
+
def prefill_error_context
|
144
|
+
RorVsWild.merge_error_context(something: "important")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
139
149
|
#### Ignore requests, jobs, exceptions and plugins
|
140
150
|
|
141
151
|
From the configuration file, you can tell RorVsWild to skip monitoring some requests, jobs, exceptions and plugins.
|
@@ -208,6 +218,30 @@ In the case you want a custom logger such as Syslog, you can only do it by initi
|
|
208
218
|
RorVsWild.start(api_key: "API_KEY", logger: Logger::Syslog.new)
|
209
219
|
```
|
210
220
|
|
221
|
+
#### Server metrics monitoring
|
222
|
+
|
223
|
+
We are adding server metrics as a beta feature.
|
224
|
+
It monitors load average, CPU, memory, swap and disk space.
|
225
|
+
For now, only Linux is supported.
|
226
|
+
It has to be explicitly enabled with a feature flag :
|
227
|
+
|
228
|
+
```yaml
|
229
|
+
# config/rorvswild.yml
|
230
|
+
production:
|
231
|
+
api_key: API_KEY
|
232
|
+
features:
|
233
|
+
- server_metrics
|
234
|
+
```
|
235
|
+
|
236
|
+
Here is the equivalent if you prefer initialising RorVsWild manually :
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# config/initializers/rorvswild.rb
|
240
|
+
RorVsWild.start(api_key: "API_KEY", features: ["server_metrics"])
|
241
|
+
```
|
242
|
+
|
243
|
+
The data are available in a server tab beside requests and jobs.
|
244
|
+
|
211
245
|
## Contributing
|
212
246
|
|
213
247
|
1. Fork it ( https://github.com/[my-github-username]/rorvswild/fork )
|
data/lib/rorvswild/agent.rb
CHANGED
@@ -16,7 +16,7 @@ module RorVsWild
|
|
16
16
|
|
17
17
|
def self.default_ignored_exceptions
|
18
18
|
if defined?(Rails)
|
19
|
-
|
19
|
+
ActionDispatch::ExceptionWrapper.rescue_responses.keys
|
20
20
|
else
|
21
21
|
[]
|
22
22
|
end
|
@@ -26,6 +26,7 @@ module RorVsWild
|
|
26
26
|
|
27
27
|
def initialize(config)
|
28
28
|
@config = self.class.default_config.merge(config)
|
29
|
+
load_features
|
29
30
|
@client = Client.new(@config)
|
30
31
|
@queue = config[:queue] || Queue.new(client)
|
31
32
|
@locator = RorVsWild::Locator.new
|
@@ -35,6 +36,12 @@ module RorVsWild
|
|
35
36
|
cleanup_data
|
36
37
|
end
|
37
38
|
|
39
|
+
def load_features
|
40
|
+
features = config[:features] || []
|
41
|
+
features.include?("server_metrics")
|
42
|
+
require "rorvswild/metrics" if features.include?("server_metrics")
|
43
|
+
end
|
44
|
+
|
38
45
|
def setup_plugins
|
39
46
|
for name in RorVsWild::Plugin.constants
|
40
47
|
next if config[:ignore_plugins] && config[:ignore_plugins].include?(name.to_s)
|
@@ -49,7 +56,7 @@ module RorVsWild
|
|
49
56
|
measure_block(code) { eval(code) }
|
50
57
|
end
|
51
58
|
|
52
|
-
def measure_block(name, kind = "code".freeze, &block)
|
59
|
+
def measure_block(name = nil, kind = "code".freeze, &block)
|
53
60
|
current_data ? measure_section(name, kind: kind, &block) : measure_job(name, &block)
|
54
61
|
end
|
55
62
|
|
@@ -113,6 +120,18 @@ module RorVsWild
|
|
113
120
|
current_data[:error]
|
114
121
|
end
|
115
122
|
|
123
|
+
def merge_error_context(hash)
|
124
|
+
self.error_context = error_context ? error_context.merge(hash) : hash
|
125
|
+
end
|
126
|
+
|
127
|
+
def error_context
|
128
|
+
current_data[:error_context] if current_data
|
129
|
+
end
|
130
|
+
|
131
|
+
def error_context=(hash)
|
132
|
+
current_data[:error_context] = hash if current_data
|
133
|
+
end
|
134
|
+
|
116
135
|
def current_data
|
117
136
|
Thread.current[:rorvswild_data]
|
118
137
|
end
|
@@ -134,6 +153,12 @@ module RorVsWild
|
|
134
153
|
config[:ignore_jobs].include?(name)
|
135
154
|
end
|
136
155
|
|
156
|
+
def os_description
|
157
|
+
@os_description ||= `uname -sr`
|
158
|
+
rescue Exception => ex
|
159
|
+
@os_description = RbConfig::CONFIG["host_os"]
|
160
|
+
end
|
161
|
+
|
137
162
|
#######################
|
138
163
|
### Private methods ###
|
139
164
|
#######################
|
@@ -162,15 +187,16 @@ module RorVsWild
|
|
162
187
|
client.post_async("/errors".freeze, error: hash)
|
163
188
|
end
|
164
189
|
|
165
|
-
def exception_to_hash(exception,
|
190
|
+
def exception_to_hash(exception, context = nil)
|
166
191
|
file, line = locator.find_most_relevant_file_and_line_from_exception(exception)
|
192
|
+
context = context ? error_context.merge(context) : error_context if error_context
|
167
193
|
{
|
168
194
|
line: line.to_i,
|
169
195
|
file: locator.relative_path(file),
|
170
196
|
message: exception.message,
|
171
197
|
backtrace: exception.backtrace || ["No backtrace"],
|
172
198
|
exception: exception.class.to_s,
|
173
|
-
extra_details:
|
199
|
+
extra_details: context,
|
174
200
|
environment: {
|
175
201
|
os: os_description,
|
176
202
|
user: Etc.getlogin,
|
@@ -186,11 +212,5 @@ module RorVsWild
|
|
186
212
|
def ignored_exception?(exception)
|
187
213
|
(config[:ignored_exceptions] || config[:ignore_exceptions]).include?(exception.class.to_s)
|
188
214
|
end
|
189
|
-
|
190
|
-
def os_description
|
191
|
-
@os_description ||= `uname -a`
|
192
|
-
rescue Exception => ex
|
193
|
-
@os_description = RUBY_PLATFORM
|
194
|
-
end
|
195
215
|
end
|
196
216
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module RorVsWild
|
2
|
+
class Metrics
|
3
|
+
class Cpu
|
4
|
+
attr_reader :user, :system, :idle, :waiting, :stolen
|
5
|
+
attr_reader :load_average, :count
|
6
|
+
|
7
|
+
def update
|
8
|
+
if vmstat = execute(:vmstat)
|
9
|
+
vmstat = vmstat.split("\n").last.split
|
10
|
+
@user = vmstat[12].to_i
|
11
|
+
@system = vmstat[13].to_i
|
12
|
+
@idle = vmstat[14].to_i
|
13
|
+
@waiting = vmstat[15].to_i
|
14
|
+
@stolen = vmstat[16].to_i
|
15
|
+
end
|
16
|
+
if uptime = execute(:uptime)
|
17
|
+
@load_average = uptime.split[-3].to_f
|
18
|
+
end
|
19
|
+
if nproc = execute(:nproc)
|
20
|
+
@count = nproc.to_i
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute(command)
|
25
|
+
`#{command}`
|
26
|
+
rescue => ex
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module RorVsWild
|
2
|
+
class Metrics
|
3
|
+
class Memory
|
4
|
+
attr_reader :ram_total, :ram_free, :ram_available, :ram_buffers, :ram_cached
|
5
|
+
attr_reader :swap_total, :swap_free
|
6
|
+
attr_reader :storage_total, :storage_used
|
7
|
+
|
8
|
+
def ram_used
|
9
|
+
ram_total - ram_available
|
10
|
+
end
|
11
|
+
|
12
|
+
def swap_used
|
13
|
+
swap_total - swap_free
|
14
|
+
end
|
15
|
+
|
16
|
+
PROC_MEMINFO = "/proc/meminfo".freeze
|
17
|
+
MEM_TOTAL = "MemTotal" # Total usable RAM (i.e., physical RAM minus a few reserved bits and the kernel binary code).
|
18
|
+
MEM_FREE = "MemFree" # The sum of LowFree+HighFree.
|
19
|
+
MEM_AVAILABLE = "MemAvailable" # An estimate of how much memory is available for starting new applications, without swapping.
|
20
|
+
BUFFERS = "Buffers" # Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).
|
21
|
+
CACHED = "Cached" # In-memory cache for files read from the disk (the page cache). Doesn't include SwapCached.
|
22
|
+
SWAP_TOTAL = "SwapTotal" # Total amount of swap space available.
|
23
|
+
SWAP_FREE = "SwapFree" # Amount of swap space that is currently unused.
|
24
|
+
|
25
|
+
def update
|
26
|
+
info = read_meminfo
|
27
|
+
@ram_total = convert_to_bytes(info[MEM_TOTAL])
|
28
|
+
@ram_free = convert_to_bytes(info[MEM_FREE])
|
29
|
+
@ram_available = convert_to_bytes(info[MEM_AVAILABLE])
|
30
|
+
@ram_buffers = convert_to_bytes(info[BUFFERS])
|
31
|
+
@ram_cached = convert_to_bytes(info[CACHED])
|
32
|
+
@swap_total = convert_to_bytes(info[SWAP_TOTAL])
|
33
|
+
@swap_free = convert_to_bytes(info[SWAP_FREE])
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def units
|
39
|
+
@units ||= {"kb" => 1000, "mb" => 1000 * 1000, "gb" => 1000 * 1000 * 1000}.freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_meminfo
|
43
|
+
return unless File.readable?(PROC_MEMINFO)
|
44
|
+
File.read(PROC_MEMINFO).split("\n").reduce({}) do |hash, line|
|
45
|
+
name, value = line.split(":")
|
46
|
+
hash[name] = value.strip
|
47
|
+
hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def convert_to_bytes(string)
|
52
|
+
value, unit = string.split
|
53
|
+
value.to_i * units[unit.downcase]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RorVsWild
|
2
|
+
class Metrics
|
3
|
+
class Storage
|
4
|
+
attr_reader :used, :free
|
5
|
+
|
6
|
+
def update
|
7
|
+
array = `df -k | grep " /$"`.split
|
8
|
+
@used = array[2].to_i * 1000
|
9
|
+
@free = array[3].to_i * 1000
|
10
|
+
end
|
11
|
+
|
12
|
+
def total
|
13
|
+
used + free
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module RorVsWild
|
2
|
+
class Metrics
|
3
|
+
UPDATE_INTERVAL_MS = 60_000 # One metric every minute
|
4
|
+
|
5
|
+
attr_reader :cpu, :memory, :storage, :updated_at
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@cpu = RorVsWild::Metrics::Cpu.new
|
9
|
+
@memory = RorVsWild::Metrics::Memory.new
|
10
|
+
@storage = RorVsWild::Metrics::Storage.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
if staled?
|
15
|
+
cpu.update
|
16
|
+
memory.update
|
17
|
+
storage.update
|
18
|
+
@updated_at = RorVsWild.clock_milliseconds
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def staled?
|
23
|
+
!updated_at || RorVsWild.clock_milliseconds - updated_at > UPDATE_INTERVAL_MS
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{
|
28
|
+
hostname: Socket.gethostname,
|
29
|
+
os: RorVsWild.agent.os_description,
|
30
|
+
cpu_user: cpu.user,
|
31
|
+
cpu_system: cpu.system,
|
32
|
+
cpu_idle: cpu.idle,
|
33
|
+
cpu_waiting: cpu.waiting,
|
34
|
+
cpu_stolen: cpu.stolen,
|
35
|
+
cpu_count: cpu.count,
|
36
|
+
load_average: cpu.load_average,
|
37
|
+
ram_total: memory.ram_total,
|
38
|
+
ram_free: memory.ram_free,
|
39
|
+
ram_used: memory.ram_used,
|
40
|
+
ram_cached: memory.ram_cached,
|
41
|
+
swap_total: memory.swap_total,
|
42
|
+
swap_used: memory.swap_used,
|
43
|
+
swap_free: memory.swap_free,
|
44
|
+
storage_total: storage.total,
|
45
|
+
storage_used: storage.used,
|
46
|
+
storage_free: storage.free,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
require "rorvswild/metrics/cpu"
|
53
|
+
require "rorvswild/metrics/memory"
|
54
|
+
require "rorvswild/metrics/storage"
|
@@ -2,7 +2,6 @@ module RorVsWild
|
|
2
2
|
module Plugin
|
3
3
|
class NetHttp
|
4
4
|
HTTP = "http".freeze
|
5
|
-
HTTPS = "https".freeze
|
6
5
|
|
7
6
|
def self.setup
|
8
7
|
return if !defined?(Net::HTTP)
|
@@ -21,9 +20,7 @@ module RorVsWild
|
|
21
20
|
|
22
21
|
def request_with_rorvswild(req, body = nil, &block)
|
23
22
|
return request_without_rorvswild(req, body, &block) if request_called_twice?
|
24
|
-
|
25
|
-
url = "#{req.method} #{scheme}://#{address}#{req.path}"
|
26
|
-
RorVsWild.agent.measure_section(url, kind: HTTP) do
|
23
|
+
RorVsWild.agent.measure_section("#{req.method} #{address}", kind: HTTP) do
|
27
24
|
request_without_rorvswild(req, body, &block)
|
28
25
|
end
|
29
26
|
end
|
@@ -18,7 +18,7 @@ module RorVsWild
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.commands_to_string(commands)
|
21
|
-
commands.map { |c| c[0]
|
21
|
+
commands.map { |c| c[0] }.join("\n".freeze)
|
22
22
|
end
|
23
23
|
|
24
24
|
APPENDABLE_COMMANDS = [:auth, :select]
|
data/lib/rorvswild/queue.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module RorVsWild
|
2
2
|
class Queue
|
3
|
-
SLEEP_TIME =
|
3
|
+
SLEEP_TIME = 30
|
4
4
|
FLUSH_TRESHOLD = 10
|
5
5
|
|
6
6
|
attr_reader :mutex, :thread, :client
|
@@ -11,6 +11,7 @@ module RorVsWild
|
|
11
11
|
@requests = []
|
12
12
|
@client = client
|
13
13
|
@mutex = Mutex.new
|
14
|
+
@metrics = RorVsWild::Metrics.new if defined?(Metrics)
|
14
15
|
Kernel.at_exit { flush }
|
15
16
|
end
|
16
17
|
|
@@ -50,6 +51,10 @@ module RorVsWild
|
|
50
51
|
result
|
51
52
|
end
|
52
53
|
|
54
|
+
def pull_server_metrics
|
55
|
+
@metrics && @metrics.update && @metrics.to_h
|
56
|
+
end
|
57
|
+
|
53
58
|
def flush_indefinetely
|
54
59
|
sleep(SLEEP_TIME) and flush while true
|
55
60
|
rescue Exception => ex
|
@@ -60,6 +65,7 @@ module RorVsWild
|
|
60
65
|
def flush
|
61
66
|
data = pull_jobs and client.post("/jobs", jobs: data)
|
62
67
|
data = pull_requests and client.post("/requests", requests: data)
|
68
|
+
data = pull_server_metrics and client.post("/metrics", metrics: data)
|
63
69
|
end
|
64
70
|
|
65
71
|
def start_thread
|
data/lib/rorvswild/version.rb
CHANGED
data/lib/rorvswild.rb
CHANGED
@@ -23,6 +23,10 @@ module RorVsWild
|
|
23
23
|
@logger ||= initialize_logger
|
24
24
|
end
|
25
25
|
|
26
|
+
def self.measure(code_or_name = nil, &block)
|
27
|
+
block ? measure_block(code_or_name, &block) : measure_code(code_or_name)
|
28
|
+
end
|
29
|
+
|
26
30
|
def self.measure_code(code)
|
27
31
|
agent ? agent.measure_code(code) : eval(code)
|
28
32
|
end
|
@@ -39,6 +43,10 @@ module RorVsWild
|
|
39
43
|
agent.record_error(exception, extra_details) if agent
|
40
44
|
end
|
41
45
|
|
46
|
+
def self.merge_error_context(hash)
|
47
|
+
agent.merge_error_context(hash) if agent
|
48
|
+
end
|
49
|
+
|
42
50
|
def self.initialize_logger(destination = nil)
|
43
51
|
if destination.respond_to?(:info) && destination.respond_to?(:warn) && destination.respond_to?(:error)
|
44
52
|
destination
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rorvswild
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexis Bernard
|
8
8
|
- Antoine Marguerie
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-08-17 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Performances and errors insights for rails developers.
|
15
15
|
email:
|
@@ -40,6 +40,10 @@ files:
|
|
40
40
|
- lib/rorvswild/local/stylesheet/local.css
|
41
41
|
- lib/rorvswild/local/stylesheet/vendor/prism.css
|
42
42
|
- lib/rorvswild/locator.rb
|
43
|
+
- lib/rorvswild/metrics.rb
|
44
|
+
- lib/rorvswild/metrics/cpu.rb
|
45
|
+
- lib/rorvswild/metrics/memory.rb
|
46
|
+
- lib/rorvswild/metrics/storage.rb
|
43
47
|
- lib/rorvswild/plugin/action_controller.rb
|
44
48
|
- lib/rorvswild/plugin/action_mailer.rb
|
45
49
|
- lib/rorvswild/plugin/action_view.rb
|
@@ -62,8 +66,10 @@ files:
|
|
62
66
|
homepage: https://www.rorvswild.com
|
63
67
|
licenses:
|
64
68
|
- MIT
|
65
|
-
metadata:
|
66
|
-
|
69
|
+
metadata:
|
70
|
+
source_code_uri: https://github.com/BaseSecrete/rorvswild
|
71
|
+
changelog_uri: https://github.com/BaseSecrete/rorvswild/blob/master/CHANGELOG.md
|
72
|
+
post_install_message:
|
67
73
|
rdoc_options: []
|
68
74
|
require_paths:
|
69
75
|
- lib
|
@@ -78,8 +84,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
84
|
- !ruby/object:Gem::Version
|
79
85
|
version: '0'
|
80
86
|
requirements: []
|
81
|
-
rubygems_version: 3.
|
82
|
-
signing_key:
|
87
|
+
rubygems_version: 3.2.22
|
88
|
+
signing_key:
|
83
89
|
specification_version: 4
|
84
90
|
summary: Ruby on Rails applications monitoring
|
85
91
|
test_files: []
|