rack-dev_insight 0.2.2.alpha.1-arm64-darwin

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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/lib/rack/dev_insight/3.0/rack_dev_insight.bundle +0 -0
  3. data/lib/rack/dev_insight/3.1/rack_dev_insight.bundle +0 -0
  4. data/lib/rack/dev_insight/3.2/rack_dev_insight.bundle +0 -0
  5. data/lib/rack/dev_insight/config.rb +43 -0
  6. data/lib/rack/dev_insight/context.rb +26 -0
  7. data/lib/rack/dev_insight/disable_net_http_patch.rb +7 -0
  8. data/lib/rack/dev_insight/enable_sql_patch.rb +8 -0
  9. data/lib/rack/dev_insight/errors.rb +13 -0
  10. data/lib/rack/dev_insight/ext/extractor.rb +21 -0
  11. data/lib/rack/dev_insight/ext/normalizer.rb +13 -0
  12. data/lib/rack/dev_insight/patches/api/net_http.rb +15 -0
  13. data/lib/rack/dev_insight/patches/sql/mysql2.rb +46 -0
  14. data/lib/rack/dev_insight/patches/sql/pg.rb +66 -0
  15. data/lib/rack/dev_insight/railtie.rb +20 -0
  16. data/lib/rack/dev_insight/recorder/api_recorder.rb +29 -0
  17. data/lib/rack/dev_insight/recorder/base_recorder.rb +30 -0
  18. data/lib/rack/dev_insight/recorder/request_recorder.rb +24 -0
  19. data/lib/rack/dev_insight/recorder/sql_recorder.rb +66 -0
  20. data/lib/rack/dev_insight/result/apis.rb +58 -0
  21. data/lib/rack/dev_insight/result/sql/crud_aggregations.rb +39 -0
  22. data/lib/rack/dev_insight/result/sql/errored_queries.rb +25 -0
  23. data/lib/rack/dev_insight/result/sql/normalized_aggregations.rb +36 -0
  24. data/lib/rack/dev_insight/result/sql/queries.rb +29 -0
  25. data/lib/rack/dev_insight/result/sql.rb +33 -0
  26. data/lib/rack/dev_insight/result.rb +78 -0
  27. data/lib/rack/dev_insight/sql_dialects.rb +30 -0
  28. data/lib/rack/dev_insight/sql_notifications.rb +35 -0
  29. data/lib/rack/dev_insight/storage/file_store.rb +38 -0
  30. data/lib/rack/dev_insight/storage/memory_store.rb +45 -0
  31. data/lib/rack/dev_insight/utils/camelizer.rb +24 -0
  32. data/lib/rack/dev_insight/version.rb +7 -0
  33. data/lib/rack/dev_insight.rb +101 -0
  34. metadata +96 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '08580682ccb5c7a62b97212740624d32efc6d83770fe3c373524d25d76d774a2'
4
+ data.tar.gz: 28417d7dc55cccfe66f3a1a2dc4424daff3e59e08e9e6982a586a842154180e0
5
+ SHA512:
6
+ metadata.gz: 7f3a2a33c87dad5be5f928524d426e47fdd5063f4e1bc6dc32ba231c80030a1869d4e5de576fb1bf8489224ef20a948faddb6f52ac0da3811d147e2dec205db7
7
+ data.tar.gz: 1c65c1ac84efc4a69c8b0570c1610e9b4d0dd1a858e832e05dcbf88f62746c9ccf9cb03e16c17797ad1b176a9d7fb9e28f2aa25e8a14ca830b12a411e738a868
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class DevInsight
5
+ DISABLE_NET_HTTP_PATCH = true
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class DevInsight
5
+ ENABLE_SQL_PATCH = true
6
+ DISABLE_SQL_SUBSCRIPTION = true
7
+ end
8
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class DevInsight
5
+ module Normalizer
6
+ class << self
7
+ def normalize(dialect_name, statement)
8
+ _normalize(dialect_name, statement)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class DevInsight
5
+ VERSION = '0.2.2.alpha.1'
6
+ end
7
+ 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.alpha.1
5
+ platform: arm64-darwin
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.bundle
37
+ - lib/rack/dev_insight/3.1/rack_dev_insight.bundle
38
+ - lib/rack/dev_insight/3.2/rack_dev_insight.bundle
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: []