rack-dev_insight 0.2.2-aarch64-linux
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/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: []
|