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

Sign up to get free protection for your applications and to get access to all the features.
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: []