rack-dev_insight 0.2.2-aarch64-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/rack/dev_insight/3.0/rack_dev_insight.so +0 -0
- data/lib/rack/dev_insight/3.1/rack_dev_insight.so +0 -0
- data/lib/rack/dev_insight/3.2/rack_dev_insight.so +0 -0
- data/lib/rack/dev_insight/config.rb +43 -0
- data/lib/rack/dev_insight/context.rb +26 -0
- data/lib/rack/dev_insight/disable_net_http_patch.rb +7 -0
- data/lib/rack/dev_insight/enable_sql_patch.rb +8 -0
- data/lib/rack/dev_insight/errors.rb +13 -0
- data/lib/rack/dev_insight/ext/extractor.rb +21 -0
- data/lib/rack/dev_insight/ext/normalizer.rb +13 -0
- data/lib/rack/dev_insight/patches/api/net_http.rb +15 -0
- data/lib/rack/dev_insight/patches/sql/mysql2.rb +46 -0
- data/lib/rack/dev_insight/patches/sql/pg.rb +66 -0
- data/lib/rack/dev_insight/railtie.rb +20 -0
- data/lib/rack/dev_insight/recorder/api_recorder.rb +29 -0
- data/lib/rack/dev_insight/recorder/base_recorder.rb +30 -0
- data/lib/rack/dev_insight/recorder/request_recorder.rb +24 -0
- data/lib/rack/dev_insight/recorder/sql_recorder.rb +66 -0
- data/lib/rack/dev_insight/result/apis.rb +58 -0
- data/lib/rack/dev_insight/result/sql/crud_aggregations.rb +39 -0
- data/lib/rack/dev_insight/result/sql/errored_queries.rb +25 -0
- data/lib/rack/dev_insight/result/sql/normalized_aggregations.rb +36 -0
- data/lib/rack/dev_insight/result/sql/queries.rb +29 -0
- data/lib/rack/dev_insight/result/sql.rb +33 -0
- data/lib/rack/dev_insight/result.rb +78 -0
- data/lib/rack/dev_insight/sql_dialects.rb +30 -0
- data/lib/rack/dev_insight/sql_notifications.rb +35 -0
- data/lib/rack/dev_insight/storage/file_store.rb +38 -0
- data/lib/rack/dev_insight/storage/memory_store.rb +45 -0
- data/lib/rack/dev_insight/utils/camelizer.rb +24 -0
- data/lib/rack/dev_insight/version.rb +7 -0
- data/lib/rack/dev_insight.rb +101 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5411e06db8ab18bc1e25d58588e78b9d9f9c7a638ccdfdd7ee2e1d37cb1f1d1a
|
4
|
+
data.tar.gz: 2aa69dbea53b9f1d3e2e8e7cb5aa85dc1fa7749f207141d5ce5669b3eb5ef213
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84c68f5fe18dd737bd467ae0d12cf595bf1eab4b643da0d64ad364ef2b924c906b2b54f4c0d7c7623e6e051268852299dabd9018bafa5ba5fc3d29be9fcc42e0
|
7
|
+
data.tar.gz: 3f3118a8c28003beacd648489f8e3bac041b3253fb229e09dc5242dc071dfb97368e9718d675a8089d4e6b25117de8c5d6abee98eb7436f925f12515ec06766b
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Config
|
6
|
+
attr_accessor :storage_type,
|
7
|
+
:memory_store_size,
|
8
|
+
:file_store_pool_size,
|
9
|
+
:file_store_dir_path,
|
10
|
+
:skip_paths,
|
11
|
+
:backtrace_exclusion_patterns,
|
12
|
+
:backtrace_depth,
|
13
|
+
:prepared_statement_limit,
|
14
|
+
:skip_cached_sql,
|
15
|
+
:detected_dialect
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@storage_type = :memory
|
19
|
+
@memory_store_size = 32 * 1024 * 1024
|
20
|
+
@file_store_pool_size = 100
|
21
|
+
@file_store_dir_path = 'tmp/rack-dev_insight'
|
22
|
+
@skip_paths = []
|
23
|
+
@backtrace_depth = 5
|
24
|
+
@backtrace_exclusion_patterns = [%r{/gems/}]
|
25
|
+
@prepared_statement_limit = 1000
|
26
|
+
@skip_cached_sql = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def storage_instance
|
30
|
+
case storage_type.to_sym
|
31
|
+
when :memory
|
32
|
+
MemoryStore.new
|
33
|
+
when :file
|
34
|
+
FileStore.new
|
35
|
+
else
|
36
|
+
warn "warning: Unknown storage type: #{storage_type} in Rack::DevInsight::Config. " \
|
37
|
+
'Available types are :memory and :file. Falling back to :memory.'
|
38
|
+
MemoryStore.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Context
|
6
|
+
class << self
|
7
|
+
def current
|
8
|
+
Thread.current[:rack_dev_insight_context]
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_current(id)
|
12
|
+
Thread.current[:rack_dev_insight_context] = new(id)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :id, :result
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def initialize(id)
|
21
|
+
@id = id
|
22
|
+
@result = Result.new(id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
# Definitions must be synced with ext/rack_dev_insight/src/errors.rs
|
6
|
+
class Error < StandardError
|
7
|
+
end
|
8
|
+
class ExtError < Error
|
9
|
+
end
|
10
|
+
class ParserError < ExtError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
module Extractor
|
6
|
+
class CrudTables
|
7
|
+
class << self
|
8
|
+
def extract(dialect_name, statement)
|
9
|
+
crud_tables = _extract(dialect_name, statement)
|
10
|
+
{
|
11
|
+
'CREATE' => crud_tables._create_tables,
|
12
|
+
'READ' => crud_tables._read_tables,
|
13
|
+
'UPDATE' => crud_tables._update_tables,
|
14
|
+
'DELETE' => crud_tables._delete_tables,
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if defined?(Net) && defined?(Net::HTTP)
|
4
|
+
module Net
|
5
|
+
class HTTP
|
6
|
+
module RackDevInsight
|
7
|
+
def request(request, *args, &block)
|
8
|
+
Rack::DevInsight::ApiRecorder.new.record(net_http: self, request: request) { super }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
prepend RackDevInsight
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if defined?(Mysql2::Client)
|
4
|
+
module Mysql2
|
5
|
+
class Client
|
6
|
+
module RackDevInsight
|
7
|
+
def query(*args, &block)
|
8
|
+
sql = args[0]
|
9
|
+
Rack::DevInsight::SqlRecorder
|
10
|
+
.new
|
11
|
+
.record_sql(dialect: Rack::DevInsight::SqlDialects::MYSQL, statement: sql) { super }
|
12
|
+
end
|
13
|
+
|
14
|
+
def prepare(*args, &block)
|
15
|
+
sql = args[0]
|
16
|
+
statement = super
|
17
|
+
statement.instance_variable_set(:@_rack_dev_insight_sql, sql)
|
18
|
+
statement
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
prepend RackDevInsight
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if defined?(Mysql2::Statement)
|
28
|
+
module Mysql2
|
29
|
+
class Statement
|
30
|
+
module RackDevInsight
|
31
|
+
def execute(*args, **kwargs)
|
32
|
+
params = args
|
33
|
+
Rack::DevInsight::SqlRecorder
|
34
|
+
.new
|
35
|
+
.record_sql(
|
36
|
+
dialect: Rack::DevInsight::SqlDialects::MYSQL,
|
37
|
+
statement: @_rack_dev_insight_sql || 'Missing prepared statement',
|
38
|
+
binds: params,
|
39
|
+
) { super }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
prepend RackDevInsight
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if defined?(PG::Connection)
|
4
|
+
module PG
|
5
|
+
class Connection
|
6
|
+
module RackDevInsight
|
7
|
+
%i[exec sync_exec async_exec async_query send_query].each do |method_name|
|
8
|
+
define_method(method_name) do |*args, &block|
|
9
|
+
sql = args[0]
|
10
|
+
Rack::DevInsight::SqlRecorder
|
11
|
+
.new
|
12
|
+
.record_sql(dialect: Rack::DevInsight::SqlDialects::POSTGRESQL, statement: sql) { super(*args, &block) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
%i[exec_params sync_exec_params async_exec_params send_query_params].each do |method_name|
|
17
|
+
define_method(method_name) do |*args, &block|
|
18
|
+
sql = args[0]
|
19
|
+
params = args[1]
|
20
|
+
Rack::DevInsight::SqlRecorder
|
21
|
+
.new
|
22
|
+
.record_sql(dialect: Rack::DevInsight::SqlDialects::POSTGRESQL, statement: sql, binds: params) do
|
23
|
+
super(*args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
%i[prepare sync_prepare async_prepare send_prepare].each do |method_name|
|
29
|
+
define_method(method_name) do |*args, &block|
|
30
|
+
name = args[0]
|
31
|
+
sql = args[1]
|
32
|
+
@_rack_dev_insight_prepared_statements ||= {}
|
33
|
+
# Remove the oldest prepared statement if limit is reached
|
34
|
+
while Rack::DevInsight.config.prepared_statement_limit <= @_rack_dev_insight_prepared_statements.size
|
35
|
+
@_rack_dev_insight_prepared_statements.shift
|
36
|
+
end
|
37
|
+
@_rack_dev_insight_prepared_statements[name] = sql
|
38
|
+
super(*args, &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Assume one of the above prepare methods was called before
|
43
|
+
%i[exec_prepared sync_exec_prepared async_exec_prepared send_query_prepared].each do |method_name|
|
44
|
+
define_method(method_name) do |*args, &block|
|
45
|
+
name = args[0]
|
46
|
+
params = args[1]
|
47
|
+
sql = @_rack_dev_insight_prepared_statements&.[](name) || missing_statement_message(name)
|
48
|
+
Rack::DevInsight::SqlRecorder
|
49
|
+
.new
|
50
|
+
.record_sql(dialect: Rack::DevInsight::SqlDialects::POSTGRESQL, statement: sql, binds: params) do
|
51
|
+
super(*args, &block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def missing_statement_message(name)
|
59
|
+
"Missing prepared statement name: #{name}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
prepend RackDevInsight
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'sql_notifications'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class DevInsight
|
7
|
+
class Railtie < ::Rails::Railtie
|
8
|
+
initializer 'rack_dev_insight.middlewares' do |app|
|
9
|
+
app.middleware.use(Rack::DevInsight)
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer 'rack_dev_insight.subscribe_events' do
|
13
|
+
unless defined?(DISABLE_SQL_SUBSCRIPTION)
|
14
|
+
DevInsight.config.detected_dialect = SqlDialects.detect_dialect
|
15
|
+
SqlNotifications.subscribe_events
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_recorder'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class DevInsight
|
7
|
+
class ApiRecorder < BaseRecorder
|
8
|
+
def record(net_http:, request:)
|
9
|
+
return yield if Context.current.nil?
|
10
|
+
|
11
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
12
|
+
response = yield
|
13
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
14
|
+
Context.current.result.add_api(
|
15
|
+
method: request.method,
|
16
|
+
url: request.uri || "#{net_http.address}:#{net_http.port}#{request.path}",
|
17
|
+
request_headers: request.each_header.map { |field, value| Result.build_header(field, value) },
|
18
|
+
request_body: request.body,
|
19
|
+
status: response.code.to_i,
|
20
|
+
response_headers: response.each_header.map { |field, value| Result.build_header(field, value) },
|
21
|
+
response_body: response.body,
|
22
|
+
backtrace: get_backtrace,
|
23
|
+
duration: format('%.2f', duration * 1000).to_f,
|
24
|
+
)
|
25
|
+
response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class BaseRecorder
|
6
|
+
private
|
7
|
+
|
8
|
+
def format_binds(binds)
|
9
|
+
if binds.nil? || binds.empty? || (binds.is_a?(Array) && binds.all? { _1.respond_to?(:empty?) && _1.empty? })
|
10
|
+
''
|
11
|
+
else
|
12
|
+
binds.to_s
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_backtrace
|
17
|
+
Kernel
|
18
|
+
.caller
|
19
|
+
.reject { |line| DevInsight.config.backtrace_exclusion_patterns.any? { |regex| line =~ regex } }
|
20
|
+
.first(DevInsight.config.backtrace_depth)
|
21
|
+
.map do |line|
|
22
|
+
if (match = line.match(/(?<path>.*):(?<line>\d+)/))
|
23
|
+
Result.build_backtrace_item(line, match[:path], match[:line].to_i)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
.compact
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_recorder'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class DevInsight
|
7
|
+
class RequestRecorder < BaseRecorder
|
8
|
+
def record(http_method:, path:)
|
9
|
+
return yield if Context.current.nil?
|
10
|
+
|
11
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
12
|
+
status, headers, body = yield
|
13
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
14
|
+
Context.current.result.set_request(
|
15
|
+
status: status,
|
16
|
+
http_method: http_method,
|
17
|
+
path: path,
|
18
|
+
duration: format('%.2f', duration * 1000).to_f,
|
19
|
+
)
|
20
|
+
[status, headers, body]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_recorder'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class DevInsight
|
7
|
+
class SqlRecorder < BaseRecorder
|
8
|
+
class << self
|
9
|
+
# @param [String] dialect 'mysql', 'postgresql' or 'sqlite' are supported
|
10
|
+
# @param [String] statement SQL statement
|
11
|
+
# @param [Array] binds SQL statement binds
|
12
|
+
# @param [Float] duration milliseconds of SQL execution time
|
13
|
+
# @return [nil]
|
14
|
+
def record(dialect:, statement:, binds: [], duration: 0.0)
|
15
|
+
new.record(dialect: dialect, statement: statement, binds: binds, duration: duration)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def record(dialect:, statement:, binds: [], duration: 0.0)
|
20
|
+
SqlDialects.validate!(dialect, ArgumentError)
|
21
|
+
return if Context.current.nil?
|
22
|
+
|
23
|
+
Context.current.result.add_sql(
|
24
|
+
dialect: dialect,
|
25
|
+
statement: statement,
|
26
|
+
binds: format_binds(binds),
|
27
|
+
backtrace: get_backtrace,
|
28
|
+
duration: format('%.2f', duration).to_f,
|
29
|
+
)
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def record_sql(dialect:, statement:, binds: [])
|
34
|
+
return yield if Context.current.nil?
|
35
|
+
|
36
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
37
|
+
res = yield
|
38
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
39
|
+
|
40
|
+
Context.current.result.add_sql(
|
41
|
+
dialect: dialect,
|
42
|
+
statement: statement,
|
43
|
+
binds: format_binds(binds),
|
44
|
+
backtrace: get_backtrace,
|
45
|
+
duration: format('%.2f', duration * 1000).to_f,
|
46
|
+
)
|
47
|
+
res
|
48
|
+
end
|
49
|
+
|
50
|
+
def record_from_event(started:, finished:, statement:, binds:, cached:)
|
51
|
+
return if Context.current.nil?
|
52
|
+
return if DevInsight.config.detected_dialect.nil?
|
53
|
+
return if DevInsight.config.skip_cached_sql && cached
|
54
|
+
|
55
|
+
Context.current.result.add_sql(
|
56
|
+
dialect: DevInsight.config.detected_dialect,
|
57
|
+
statement: statement,
|
58
|
+
binds: format_binds(binds),
|
59
|
+
backtrace: get_backtrace,
|
60
|
+
duration: format('%.2f', (finished - started) * 1000).to_f,
|
61
|
+
)
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Result
|
6
|
+
class Apis
|
7
|
+
Api =
|
8
|
+
Struct.new(
|
9
|
+
:id,
|
10
|
+
:method, # rubocop:disable Lint/StructNewOverride
|
11
|
+
:url,
|
12
|
+
:request_headers,
|
13
|
+
:request_body,
|
14
|
+
:status,
|
15
|
+
:response_headers,
|
16
|
+
:response_body,
|
17
|
+
:backtrace,
|
18
|
+
:duration,
|
19
|
+
)
|
20
|
+
Header = Struct.new(:field, :value)
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@id = 0
|
24
|
+
@data = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(
|
28
|
+
method,
|
29
|
+
url,
|
30
|
+
request_headers,
|
31
|
+
request_body,
|
32
|
+
status,
|
33
|
+
response_headers,
|
34
|
+
response_body,
|
35
|
+
backtrace,
|
36
|
+
duration
|
37
|
+
)
|
38
|
+
@data << Api.new(
|
39
|
+
@id += 1,
|
40
|
+
method,
|
41
|
+
url,
|
42
|
+
request_headers.map(&:to_h),
|
43
|
+
request_body,
|
44
|
+
status,
|
45
|
+
response_headers.map(&:to_h),
|
46
|
+
response_body,
|
47
|
+
backtrace.map(&:to_h),
|
48
|
+
duration,
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def attributes
|
53
|
+
@data.map(&:to_h)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Result
|
6
|
+
class Sql
|
7
|
+
class CrudAggregations
|
8
|
+
CrudAggregation = Struct.new(:id, :type, :table, :count, :duration, :query_ids) # rubocop:disable Lint/StructNewOverride
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@id = 0
|
12
|
+
@cached_data = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(dialect_name, statement, duration, query_id)
|
16
|
+
crud_tables = Extractor::CrudTables.extract(dialect_name, statement)
|
17
|
+
|
18
|
+
crud_tables.each do |type, tables|
|
19
|
+
tables.each do |table|
|
20
|
+
key = "#{type}_#{table.downcase}"
|
21
|
+
data = @cached_data[key] ||= CrudAggregation.new(@id += 1, type, table, 0, 0, [])
|
22
|
+
data.count += 1
|
23
|
+
data.duration += duration
|
24
|
+
data.query_ids << query_id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes
|
30
|
+
@cached_data.values.map do |data|
|
31
|
+
data.duration = format('%.2f', data.duration).to_f
|
32
|
+
data.to_h
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Result
|
6
|
+
class Sql
|
7
|
+
class ErroredQueries
|
8
|
+
ErroredQuery = Struct.new(:id, :message, :statement, :backtrace, :duration)
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@data = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(id, message, statement, backtrace, duration)
|
15
|
+
@data << ErroredQuery.new(id, message, statement, backtrace, duration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
@data.map(&:to_h)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Result
|
6
|
+
class Sql
|
7
|
+
class NormalizedAggregations
|
8
|
+
NormalizedAggregation = Struct.new(:id, :statement, :count, :duration, :query_ids) # rubocop:disable Lint/StructNewOverride
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@id = 0
|
12
|
+
@cached_data = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(dialect_name, statement, duration, query_id)
|
16
|
+
normalized_statements = Normalizer.normalize(dialect_name, statement)
|
17
|
+
normalized_statement = normalized_statements.join('; ')
|
18
|
+
|
19
|
+
data =
|
20
|
+
@cached_data[normalized_statement] ||= NormalizedAggregation.new(@id += 1, normalized_statement, 0, 0, [])
|
21
|
+
data.count += 1
|
22
|
+
data.duration += duration
|
23
|
+
data.query_ids << query_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def attributes
|
27
|
+
@cached_data.values.map do |data|
|
28
|
+
data.duration = format('%.2f', data.duration).to_f
|
29
|
+
data.to_h
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Result
|
6
|
+
class Sql
|
7
|
+
class Queries
|
8
|
+
Query = Struct.new(:id, :statement, :binds, :backtrace, :duration)
|
9
|
+
TraceInfo = Struct.new(:original, :path, :line)
|
10
|
+
|
11
|
+
attr_reader :id
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@id = 0
|
15
|
+
@data = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(statement, binds, backtrace, duration)
|
19
|
+
@data << Query.new(@id += 1, statement, binds, backtrace.map(&:to_h), duration)
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes
|
23
|
+
@data.map(&:to_h)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class Result
|
6
|
+
class Sql
|
7
|
+
def initialize
|
8
|
+
@crud_aggregations = CrudAggregations.new
|
9
|
+
@normalized_aggregations = NormalizedAggregations.new
|
10
|
+
@errored_queries = ErroredQueries.new
|
11
|
+
@queries = Queries.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(dialect, statement, binds, backtrace, duration)
|
15
|
+
@queries.add(statement, binds, backtrace, duration)
|
16
|
+
@crud_aggregations.add(dialect, statement, duration, @queries.id)
|
17
|
+
@normalized_aggregations.add(dialect, statement, duration, @queries.id)
|
18
|
+
rescue ExtError => e
|
19
|
+
@errored_queries.add(@queries.id, e.message, statement, backtrace, duration)
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes
|
23
|
+
{
|
24
|
+
crud_aggregations: @crud_aggregations.attributes,
|
25
|
+
normalized_aggregations: @normalized_aggregations.attributes,
|
26
|
+
errored_queries: @errored_queries.attributes,
|
27
|
+
queries: @queries.attributes,
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class DevInsight
|
7
|
+
class Result
|
8
|
+
Request = Struct.new(:status, :http_method, :path, :duration)
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def build_backtrace_item(original, path, line)
|
12
|
+
Sql::Queries::TraceInfo.new(original, path, line)
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_header(field, value)
|
16
|
+
Apis::Header.new(field, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
def initialize(id)
|
23
|
+
@id = id
|
24
|
+
@request = Request.new
|
25
|
+
@sql = Sql.new
|
26
|
+
@apis = Apis.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_request(status:, http_method:, path:, duration:)
|
30
|
+
@request = Request.new(status, http_method, path, duration)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_sql(dialect:, statement:, binds:, backtrace:, duration:)
|
34
|
+
@sql.add(dialect, statement, binds, backtrace, duration)
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_api(
|
38
|
+
method:,
|
39
|
+
url:,
|
40
|
+
request_headers:,
|
41
|
+
request_body:,
|
42
|
+
status:,
|
43
|
+
response_headers:,
|
44
|
+
response_body:,
|
45
|
+
backtrace:,
|
46
|
+
duration:
|
47
|
+
)
|
48
|
+
@apis.add(
|
49
|
+
method,
|
50
|
+
url,
|
51
|
+
request_headers,
|
52
|
+
request_body,
|
53
|
+
status,
|
54
|
+
response_headers,
|
55
|
+
response_body,
|
56
|
+
backtrace,
|
57
|
+
duration,
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def attributes
|
62
|
+
{
|
63
|
+
id: @id,
|
64
|
+
status: @request.status,
|
65
|
+
method: @request.http_method,
|
66
|
+
path: @request.path,
|
67
|
+
duration: @request.duration,
|
68
|
+
sql: @sql.attributes,
|
69
|
+
apis: @apis.attributes,
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_response_json
|
74
|
+
Camelizer.camelize_keys(attributes).to_json
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
module SqlDialects
|
6
|
+
MYSQL = 'mysql'
|
7
|
+
POSTGRESQL = 'postgresql'
|
8
|
+
SQLITE = 'sqlite'
|
9
|
+
DIALECTS = [MYSQL, POSTGRESQL, SQLITE].freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def detect_dialect
|
13
|
+
if defined?(Mysql2)
|
14
|
+
MYSQL
|
15
|
+
elsif defined?(PG)
|
16
|
+
POSTGRESQL
|
17
|
+
elsif defined?(SQLite3)
|
18
|
+
SQLITE
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate!(dialect, error_klass)
|
23
|
+
return if DIALECTS.include?(dialect)
|
24
|
+
|
25
|
+
raise error_klass, "Unsupported SQL dialect: #{dialect}. Supported dialects are: #{DIALECTS.join(', ')}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class SqlNotifications
|
6
|
+
DEFAULT_EVENT_NAMES = %w[sql.active_record sql.rom sql.sequel].freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def subscribe_events(event_names = DEFAULT_EVENT_NAMES)
|
10
|
+
event_names.each { |event_name| subscribe(event_name) }
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param [String] event_name
|
14
|
+
def subscribe(event_name)
|
15
|
+
ActiveSupport::Notifications.subscribe(event_name, &new)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_proc
|
20
|
+
method(:call).to_proc
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(*args)
|
24
|
+
_name, started, finished, _unique_id, data = args
|
25
|
+
SqlRecorder.new.record_from_event(
|
26
|
+
started: started,
|
27
|
+
finished: finished,
|
28
|
+
statement: data[:sql],
|
29
|
+
binds: data[:type_casted_binds].try(:call) || data[:type_casted_binds],
|
30
|
+
cached: data[:cached],
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class FileStore
|
6
|
+
def initialize
|
7
|
+
@dir_path = DevInsight.config.file_store_dir_path
|
8
|
+
@pool_size = DevInsight.config.file_store_pool_size
|
9
|
+
end
|
10
|
+
|
11
|
+
def write(result)
|
12
|
+
::File.open(file_path(result.id), 'wb+') do |f|
|
13
|
+
f.sync = true
|
14
|
+
f.write Marshal.dump(result)
|
15
|
+
end
|
16
|
+
reap_excess_files
|
17
|
+
end
|
18
|
+
|
19
|
+
def read(id)
|
20
|
+
return nil unless ::File.exist?(file_path(id))
|
21
|
+
|
22
|
+
Marshal.load(::File.binread(file_path(id))) # rubocop:disable Security/MarshalLoad
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def file_path(id)
|
28
|
+
FileUtils.mkdir_p(@dir_path)
|
29
|
+
Pathname.new(@dir_path).join(id.to_s).to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def reap_excess_files
|
33
|
+
files = Dir.glob(::File.join(@dir_path, '*')).sort_by { |f| ::File.mtime(f) }
|
34
|
+
files[0..-(@pool_size + 1)].each { |old_file| ::File.delete(old_file) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
class MemoryStore
|
6
|
+
MAX_REAP_SEC = 2
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@lock = Mutex.new
|
10
|
+
@cache = {}
|
11
|
+
@memory_size = DevInsight.config.memory_store_size
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(result)
|
15
|
+
@lock.synchronize do
|
16
|
+
@cache[result.id] = result
|
17
|
+
reap_excess_memory
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(id)
|
22
|
+
@lock.synchronize { @cache[id] }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def reap_excess_memory
|
28
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
29
|
+
@cache.delete(@cache.keys.first) while memory_excess? && within_reap_time?(start_time)
|
30
|
+
end
|
31
|
+
|
32
|
+
def memory_excess?
|
33
|
+
bytesize(@cache) > @memory_size
|
34
|
+
end
|
35
|
+
|
36
|
+
def within_reap_time?(start_time)
|
37
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time < MAX_REAP_SEC
|
38
|
+
end
|
39
|
+
|
40
|
+
def bytesize(data)
|
41
|
+
Marshal.dump(data).bytesize
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class DevInsight
|
5
|
+
module Camelizer
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def camelize_keys(value)
|
9
|
+
case value
|
10
|
+
when Array
|
11
|
+
value.map { |v| camelize_keys(v) }
|
12
|
+
when Hash
|
13
|
+
value.transform_keys { |key| to_camel_case(key.to_s) }.transform_values { |v| camelize_keys(v) }
|
14
|
+
else
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_camel_case(str)
|
20
|
+
str.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require_relative 'dev_insight/ext/extractor'
|
5
|
+
require_relative 'dev_insight/ext/normalizer'
|
6
|
+
require_relative 'dev_insight/recorder/api_recorder'
|
7
|
+
require_relative 'dev_insight/recorder/request_recorder'
|
8
|
+
require_relative 'dev_insight/recorder/sql_recorder'
|
9
|
+
require_relative 'dev_insight/result'
|
10
|
+
require_relative 'dev_insight/result/apis'
|
11
|
+
require_relative 'dev_insight/result/sql'
|
12
|
+
require_relative 'dev_insight/result/sql/crud_aggregations'
|
13
|
+
require_relative 'dev_insight/result/sql/errored_queries'
|
14
|
+
require_relative 'dev_insight/result/sql/normalized_aggregations'
|
15
|
+
require_relative 'dev_insight/result/sql/queries'
|
16
|
+
require_relative 'dev_insight/storage/file_store'
|
17
|
+
require_relative 'dev_insight/storage/memory_store'
|
18
|
+
require_relative 'dev_insight/utils/camelizer'
|
19
|
+
require_relative 'dev_insight/config'
|
20
|
+
require_relative 'dev_insight/context'
|
21
|
+
require_relative 'dev_insight/errors'
|
22
|
+
require_relative 'dev_insight/sql_dialects'
|
23
|
+
require_relative 'dev_insight/version'
|
24
|
+
|
25
|
+
# https://github.com/rake-compiler/rake-compiler/blob/master/README.md
|
26
|
+
# Technique to lookup the fat binaries first, and then lookup the gems compiled by the end user.
|
27
|
+
begin
|
28
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
29
|
+
require_relative "dev_insight/#{Regexp.last_match(1)}/rack_dev_insight"
|
30
|
+
rescue LoadError
|
31
|
+
require_relative 'dev_insight/rack_dev_insight'
|
32
|
+
end
|
33
|
+
# Railtie
|
34
|
+
require_relative 'dev_insight/railtie' if defined?(Rails)
|
35
|
+
# Patches
|
36
|
+
if defined?(Rack::DevInsight::ENABLE_SQL_PATCH)
|
37
|
+
require_relative 'dev_insight/patches/sql/mysql2'
|
38
|
+
require_relative 'dev_insight/patches/sql/pg'
|
39
|
+
end
|
40
|
+
require_relative 'dev_insight/patches/api/net_http' unless defined?(Rack::DevInsight::DISABLE_NET_HTTP_PATCH)
|
41
|
+
|
42
|
+
module Rack
|
43
|
+
class DevInsight
|
44
|
+
class << self
|
45
|
+
def configure
|
46
|
+
yield config
|
47
|
+
end
|
48
|
+
|
49
|
+
def config
|
50
|
+
@config ||= Config.new
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(app)
|
55
|
+
@app = app
|
56
|
+
@storage = DevInsight.config.storage_instance
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(env)
|
60
|
+
if (id = get_id_from_path(env))
|
61
|
+
fetch_analyzed(id)
|
62
|
+
else
|
63
|
+
analyze(env)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def get_id_from_path(env)
|
70
|
+
env['PATH_INFO'][%r{/rack-dev-insight-results/(.+)$}, 1]
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_analyzed(id)
|
74
|
+
header = { 'Content-Type' => 'application/json' }
|
75
|
+
if (result = @storage.read(id))
|
76
|
+
[200, header, [result.to_response_json]]
|
77
|
+
else
|
78
|
+
[404, header, [{ status: 404, message: "id: #{id} is not found" }.to_json]]
|
79
|
+
end
|
80
|
+
rescue StandardError => e
|
81
|
+
[500, header, [{ status: 500, message: e.inspect }.to_json]]
|
82
|
+
end
|
83
|
+
|
84
|
+
def analyze(env)
|
85
|
+
return @app.call(env) if skip_path?(env)
|
86
|
+
|
87
|
+
Context.create_current(SecureRandom.uuid)
|
88
|
+
request = Rack::Request.new(env)
|
89
|
+
status, headers, body =
|
90
|
+
RequestRecorder.new.record(http_method: request.request_method, path: request.fullpath) { @app.call(env) }
|
91
|
+
@storage.write(Context.current.result)
|
92
|
+
headers['X-Rack-Dev-Insight-Id'] = Context.current.id
|
93
|
+
|
94
|
+
[status, headers, body]
|
95
|
+
end
|
96
|
+
|
97
|
+
def skip_path?(env)
|
98
|
+
DevInsight.config.skip_paths.any? { |path| env['PATH_INFO'] =~ path }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-dev_insight
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
platform: aarch64-linux
|
6
|
+
authors:
|
7
|
+
- Takahiro Ebato
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-01-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
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
|
+
description: A rack middleware for analyzing SQL queries and HTTP request / response
|
28
|
+
data. Chrome extension is needed to display the analysis result.
|
29
|
+
email:
|
30
|
+
- takahiro.ebato@gmail.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- lib/rack/dev_insight.rb
|
36
|
+
- lib/rack/dev_insight/3.0/rack_dev_insight.so
|
37
|
+
- lib/rack/dev_insight/3.1/rack_dev_insight.so
|
38
|
+
- lib/rack/dev_insight/3.2/rack_dev_insight.so
|
39
|
+
- lib/rack/dev_insight/config.rb
|
40
|
+
- lib/rack/dev_insight/context.rb
|
41
|
+
- lib/rack/dev_insight/disable_net_http_patch.rb
|
42
|
+
- lib/rack/dev_insight/enable_sql_patch.rb
|
43
|
+
- lib/rack/dev_insight/errors.rb
|
44
|
+
- lib/rack/dev_insight/ext/extractor.rb
|
45
|
+
- lib/rack/dev_insight/ext/normalizer.rb
|
46
|
+
- lib/rack/dev_insight/patches/api/net_http.rb
|
47
|
+
- lib/rack/dev_insight/patches/sql/mysql2.rb
|
48
|
+
- lib/rack/dev_insight/patches/sql/pg.rb
|
49
|
+
- lib/rack/dev_insight/railtie.rb
|
50
|
+
- lib/rack/dev_insight/recorder/api_recorder.rb
|
51
|
+
- lib/rack/dev_insight/recorder/base_recorder.rb
|
52
|
+
- lib/rack/dev_insight/recorder/request_recorder.rb
|
53
|
+
- lib/rack/dev_insight/recorder/sql_recorder.rb
|
54
|
+
- lib/rack/dev_insight/result.rb
|
55
|
+
- lib/rack/dev_insight/result/apis.rb
|
56
|
+
- lib/rack/dev_insight/result/sql.rb
|
57
|
+
- lib/rack/dev_insight/result/sql/crud_aggregations.rb
|
58
|
+
- lib/rack/dev_insight/result/sql/errored_queries.rb
|
59
|
+
- lib/rack/dev_insight/result/sql/normalized_aggregations.rb
|
60
|
+
- lib/rack/dev_insight/result/sql/queries.rb
|
61
|
+
- lib/rack/dev_insight/sql_dialects.rb
|
62
|
+
- lib/rack/dev_insight/sql_notifications.rb
|
63
|
+
- lib/rack/dev_insight/storage/file_store.rb
|
64
|
+
- lib/rack/dev_insight/storage/memory_store.rb
|
65
|
+
- lib/rack/dev_insight/utils/camelizer.rb
|
66
|
+
- lib/rack/dev_insight/version.rb
|
67
|
+
homepage: https://github.com/takaebato/rack-dev_insight
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata:
|
71
|
+
source_code_uri: https://github.com/takaebato/rack-dev_insight
|
72
|
+
changelog_uri: https://github.com/takaebato/rack-dev_insight/blob/master/CHANGELOG.md
|
73
|
+
rubygems_mfa_required: 'true'
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- - "<"
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.3.dev
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 3.3.22
|
91
|
+
requirements: []
|
92
|
+
rubygems_version: 3.4.4
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: A rack middleware for analyzing SQL queries and HTTP request / response data.
|
96
|
+
test_files: []
|