logstruct 0.0.1 → 0.0.2.pre.rc2

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/LICENSE +21 -0
  4. data/README.md +67 -0
  5. data/lib/log_struct/concerns/configuration.rb +93 -0
  6. data/lib/log_struct/concerns/error_handling.rb +94 -0
  7. data/lib/log_struct/concerns/logging.rb +45 -0
  8. data/lib/log_struct/config_struct/error_handling_modes.rb +25 -0
  9. data/lib/log_struct/config_struct/filters.rb +80 -0
  10. data/lib/log_struct/config_struct/integrations.rb +89 -0
  11. data/lib/log_struct/configuration.rb +59 -0
  12. data/lib/log_struct/enums/error_handling_mode.rb +22 -0
  13. data/lib/log_struct/enums/error_reporter.rb +14 -0
  14. data/lib/log_struct/enums/event.rb +48 -0
  15. data/lib/log_struct/enums/level.rb +66 -0
  16. data/lib/log_struct/enums/source.rb +26 -0
  17. data/lib/log_struct/enums.rb +9 -0
  18. data/lib/log_struct/formatter.rb +224 -0
  19. data/lib/log_struct/handlers.rb +27 -0
  20. data/lib/log_struct/hash_utils.rb +21 -0
  21. data/lib/log_struct/integrations/action_mailer/callbacks.rb +100 -0
  22. data/lib/log_struct/integrations/action_mailer/error_handling.rb +173 -0
  23. data/lib/log_struct/integrations/action_mailer/event_logging.rb +90 -0
  24. data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +78 -0
  25. data/lib/log_struct/integrations/action_mailer.rb +50 -0
  26. data/lib/log_struct/integrations/active_job/log_subscriber.rb +104 -0
  27. data/lib/log_struct/integrations/active_job.rb +38 -0
  28. data/lib/log_struct/integrations/active_record.rb +258 -0
  29. data/lib/log_struct/integrations/active_storage.rb +94 -0
  30. data/lib/log_struct/integrations/carrierwave.rb +111 -0
  31. data/lib/log_struct/integrations/good_job/log_subscriber.rb +228 -0
  32. data/lib/log_struct/integrations/good_job/logger.rb +73 -0
  33. data/lib/log_struct/integrations/good_job.rb +111 -0
  34. data/lib/log_struct/integrations/host_authorization.rb +81 -0
  35. data/lib/log_struct/integrations/integration_interface.rb +21 -0
  36. data/lib/log_struct/integrations/lograge.rb +114 -0
  37. data/lib/log_struct/integrations/rack.rb +31 -0
  38. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +146 -0
  39. data/lib/log_struct/integrations/rack_error_handler.rb +32 -0
  40. data/lib/log_struct/integrations/shrine.rb +75 -0
  41. data/lib/log_struct/integrations/sidekiq/logger.rb +43 -0
  42. data/lib/log_struct/integrations/sidekiq.rb +39 -0
  43. data/lib/log_struct/integrations/sorbet.rb +49 -0
  44. data/lib/log_struct/integrations.rb +41 -0
  45. data/lib/log_struct/log/action_mailer.rb +55 -0
  46. data/lib/log_struct/log/active_job.rb +64 -0
  47. data/lib/log_struct/log/active_storage.rb +78 -0
  48. data/lib/log_struct/log/carrierwave.rb +82 -0
  49. data/lib/log_struct/log/error.rb +76 -0
  50. data/lib/log_struct/log/good_job.rb +151 -0
  51. data/lib/log_struct/log/interfaces/additional_data_field.rb +20 -0
  52. data/lib/log_struct/log/interfaces/common_fields.rb +42 -0
  53. data/lib/log_struct/log/interfaces/message_field.rb +20 -0
  54. data/lib/log_struct/log/interfaces/request_fields.rb +36 -0
  55. data/lib/log_struct/log/plain.rb +53 -0
  56. data/lib/log_struct/log/request.rb +76 -0
  57. data/lib/log_struct/log/security.rb +80 -0
  58. data/lib/log_struct/log/shared/add_request_fields.rb +29 -0
  59. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +28 -0
  60. data/lib/log_struct/log/shared/serialize_common.rb +36 -0
  61. data/lib/log_struct/log/shrine.rb +70 -0
  62. data/lib/log_struct/log/sidekiq.rb +50 -0
  63. data/lib/log_struct/log/sql.rb +126 -0
  64. data/lib/log_struct/log.rb +43 -0
  65. data/lib/log_struct/log_keys.rb +102 -0
  66. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +36 -0
  67. data/lib/log_struct/multi_error_reporter.rb +149 -0
  68. data/lib/log_struct/param_filters.rb +89 -0
  69. data/lib/log_struct/railtie.rb +31 -0
  70. data/lib/log_struct/semantic_logger/color_formatter.rb +209 -0
  71. data/lib/log_struct/semantic_logger/formatter.rb +94 -0
  72. data/lib/log_struct/semantic_logger/logger.rb +129 -0
  73. data/lib/log_struct/semantic_logger/setup.rb +219 -0
  74. data/lib/log_struct/sorbet/serialize_symbol_keys.rb +23 -0
  75. data/lib/log_struct/sorbet.rb +13 -0
  76. data/lib/log_struct/string_scrubber.rb +84 -0
  77. data/lib/log_struct/version.rb +6 -0
  78. data/lib/log_struct.rb +37 -0
  79. data/lib/logstruct.rb +2 -6
  80. data/logstruct.gemspec +52 -0
  81. metadata +221 -5
  82. data/Rakefile +0 -5
@@ -0,0 +1,90 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module Integrations
6
+ module ActionMailer
7
+ # Handles logging of email delivery events
8
+ module EventLogging
9
+ extend ActiveSupport::Concern
10
+ extend T::Sig
11
+ extend T::Helpers
12
+ requires_ancestor { ::ActionMailer::Base }
13
+
14
+ included do
15
+ T.bind(self, ActionMailer::Callbacks::ClassMethods)
16
+
17
+ # Add callbacks for delivery events
18
+ before_deliver :log_email_delivery
19
+ after_deliver :log_email_delivered
20
+ end
21
+
22
+ protected
23
+
24
+ # Log when an email is about to be delivered
25
+ sig { void }
26
+ def log_email_delivery
27
+ log_mailer_event(Event::Delivery)
28
+ end
29
+
30
+ # Log when an email is delivered
31
+ sig { void }
32
+ def log_email_delivered
33
+ log_mailer_event(Event::Delivered)
34
+ end
35
+
36
+ private
37
+
38
+ # Log a mailer event with the given event type
39
+ sig do
40
+ params(event_type: Log::ActionMailer::ActionMailerEvent,
41
+ level: Symbol,
42
+ additional_data: T::Hash[Symbol, T.untyped]).returns(T.untyped)
43
+ end
44
+ def log_mailer_event(event_type, level = :info, additional_data = {})
45
+ # Get message (self refers to the mailer instance)
46
+ mailer_message = message if respond_to?(:message)
47
+
48
+ # Prepare data for the log entry
49
+ data = {
50
+ message_id: extract_message_id,
51
+ mailer_class: self.class.to_s,
52
+ mailer_action: action_name.to_s
53
+ }.compact
54
+
55
+ # Add any additional metadata
56
+ MetadataCollection.add_message_metadata(self, data)
57
+ MetadataCollection.add_context_metadata(self, data)
58
+ data.merge!(additional_data) if additional_data.present?
59
+
60
+ # Extract email fields (these will be filtered if email_addresses=true)
61
+ to = mailer_message&.to
62
+ from = mailer_message&.from&.first
63
+ subject = mailer_message&.subject
64
+
65
+ # Create a structured log entry
66
+ log_data = Log::ActionMailer.new(
67
+ event: event_type,
68
+ to: to,
69
+ from: from,
70
+ subject: subject,
71
+ additional_data: data
72
+ )
73
+ LogStruct.info(log_data)
74
+ log_data
75
+ end
76
+
77
+ # Extract message ID from the mailer
78
+ sig { returns(T.nilable(String)) }
79
+ def extract_message_id
80
+ return nil unless respond_to?(:message)
81
+
82
+ mail_message = message
83
+ return nil unless mail_message.respond_to?(:message_id)
84
+
85
+ mail_message.message_id
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,78 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module Integrations
6
+ module ActionMailer
7
+ # Handles collection of metadata for email logging
8
+ module MetadataCollection
9
+ extend T::Sig
10
+ # Add message-specific metadata to log data
11
+ sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
12
+ def self.add_message_metadata(mailer, log_data)
13
+ message = mailer.respond_to?(:message) ? mailer.message : nil
14
+
15
+ # Add recipient count if message is available
16
+ if message
17
+ # Don't log actual email addresses
18
+ log_data[:recipient_count] = [message.to, message.cc, message.bcc].flatten.compact.count
19
+
20
+ # Handle case when attachments might be nil
21
+ log_data[:has_attachments] = message.attachments&.any? || false
22
+ log_data[:attachment_count] = message.attachments&.count || 0
23
+ else
24
+ log_data[:recipient_count] = 0
25
+ log_data[:has_attachments] = false
26
+ log_data[:attachment_count] = 0
27
+ end
28
+ end
29
+
30
+ # Add context metadata to log data
31
+ sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
32
+ def self.add_context_metadata(mailer, log_data)
33
+ # Add account ID information if available (but not user email)
34
+ extract_ids_to_log_data(mailer, log_data)
35
+
36
+ # Add any current tags from ActiveJob or ActionMailer
37
+ add_current_tags_to_log_data(log_data)
38
+ end
39
+
40
+ sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
41
+ def self.extract_ids_to_log_data(mailer, log_data)
42
+ # Extract account ID if available
43
+ if mailer.instance_variable_defined?(:@account)
44
+ account = mailer.instance_variable_get(:@account)
45
+ log_data[:account_id] = account.id if account.respond_to?(:id)
46
+ end
47
+
48
+ # Extract user ID if available
49
+ return unless mailer.instance_variable_defined?(:@user)
50
+
51
+ user = mailer.instance_variable_get(:@user)
52
+ log_data[:user_id] = user.id if user.respond_to?(:id)
53
+ end
54
+
55
+ sig { params(log_data: T::Hash[Symbol, T.untyped]).void }
56
+ def self.add_current_tags_to_log_data(log_data)
57
+ # Get current tags from ActiveSupport::TaggedLogging if available
58
+ if ::ActiveSupport::TaggedLogging.respond_to?(:current_tags)
59
+ tags = T.unsafe(::ActiveSupport::TaggedLogging).current_tags
60
+ log_data[:tags] = tags if tags.present?
61
+ end
62
+
63
+ # Get request_id from ActionDispatch if available
64
+ if ::ActionDispatch::Request.respond_to?(:current_request_id) &&
65
+ T.unsafe(::ActionDispatch::Request).current_request_id.present?
66
+ log_data[:request_id] = T.unsafe(::ActionDispatch::Request).current_request_id
67
+ end
68
+
69
+ # Get job_id from ActiveJob if available
70
+ if defined?(::ActiveJob::Logging) && ::ActiveJob::Logging.respond_to?(:job_id) &&
71
+ T.unsafe(::ActiveJob::Logging).job_id.present?
72
+ log_data[:job_id] = T.unsafe(::ActiveJob::Logging).job_id
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "action_mailer"
6
+ rescue LoadError
7
+ # actionmailer gem is not available, integration will be skipped
8
+ end
9
+
10
+ if defined?(::ActionMailer)
11
+ require "logger"
12
+ require_relative "action_mailer/metadata_collection"
13
+ require_relative "action_mailer/event_logging"
14
+ require_relative "action_mailer/error_handling"
15
+ require_relative "action_mailer/callbacks"
16
+ end
17
+
18
+ module LogStruct
19
+ module Integrations
20
+ # ActionMailer integration for structured logging
21
+ module ActionMailer
22
+ extend T::Sig
23
+ extend IntegrationInterface
24
+
25
+ # Set up ActionMailer structured logging
26
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
27
+ def self.setup(config)
28
+ return nil unless defined?(::ActionMailer)
29
+ return nil unless config.enabled
30
+ return nil unless config.integrations.enable_actionmailer
31
+
32
+ # Silence default ActionMailer logs (we use our own structured logging)
33
+ # This is required because we replace the logging using our own callbacks
34
+ if defined?(::ActionMailer::Base)
35
+ ::ActionMailer::Base.logger = ::Logger.new(File::NULL)
36
+ end
37
+
38
+ # Register our custom observers and handlers
39
+ # Registering these at the class level means all mailers will use them
40
+ ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::MetadataCollection }
41
+ ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::EventLogging }
42
+ ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::ErrorHandling }
43
+ ActiveSupport.on_load(:action_mailer) { prepend LogStruct::Integrations::ActionMailer::Callbacks }
44
+ ActiveSupport.on_load(:action_mailer) { LogStruct::Integrations::ActionMailer::Callbacks.patch_message_delivery }
45
+
46
+ true
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,104 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../enums/source"
5
+ require_relative "../../enums/event"
6
+ require_relative "../../log/active_job"
7
+ require_relative "../../log/error"
8
+
9
+ module LogStruct
10
+ module Integrations
11
+ module ActiveJob
12
+ # Structured logging for ActiveJob
13
+ class LogSubscriber < ::ActiveJob::LogSubscriber
14
+ extend T::Sig
15
+
16
+ sig { params(event: T.untyped).void }
17
+ def enqueue(event)
18
+ job = event.payload[:job]
19
+ log_job_event(Event::Enqueue, job, event)
20
+ end
21
+
22
+ sig { params(event: T.untyped).void }
23
+ def enqueue_at(event)
24
+ job = event.payload[:job]
25
+ log_job_event(Event::Schedule, job, event, scheduled_at: job.scheduled_at)
26
+ end
27
+
28
+ sig { params(event: T.untyped).void }
29
+ def perform(event)
30
+ job = event.payload[:job]
31
+ exception = event.payload[:exception_object]
32
+
33
+ if exception
34
+ # Log the exception with the job context
35
+ log_exception(exception, job, event)
36
+ else
37
+ log_job_event(Event::Finish, job, event, duration: event.duration.round(2))
38
+ end
39
+ end
40
+
41
+ sig { params(event: T.untyped).void }
42
+ def perform_start(event)
43
+ job = event.payload[:job]
44
+ log_job_event(Event::Start, job, event)
45
+ end
46
+
47
+ private
48
+
49
+ sig { params(event_type: T.any(Event::Enqueue, Event::Schedule, Event::Start, Event::Finish), job: T.untyped, _event: T.untyped, additional_data: T::Hash[Symbol, T.untyped]).void }
50
+ def log_job_event(event_type, job, _event, additional_data = {})
51
+ # Create structured log data
52
+ log_data = Log::ActiveJob.new(
53
+ event: event_type,
54
+ job_id: job.job_id,
55
+ job_class: job.class.to_s,
56
+ queue_name: job.queue_name,
57
+ duration: additional_data[:duration],
58
+ # Add arguments if the job class allows it
59
+ arguments: job.class.log_arguments? ? job.arguments : nil,
60
+ # Store additional data in the data hash
61
+ additional_data: {
62
+ executions: job.executions,
63
+ scheduled_at: additional_data[:scheduled_at],
64
+ provider_job_id: job.provider_job_id
65
+ }.compact
66
+ )
67
+
68
+ # Use Rails logger with our structured formatter
69
+ logger.info(log_data)
70
+ end
71
+
72
+ sig { params(exception: StandardError, job: T.untyped, _event: T.untyped).void }
73
+ def log_exception(exception, job, _event)
74
+ # Create job context data for the exception
75
+ job_context = {
76
+ job_id: job.job_id,
77
+ job_class: job.class.to_s,
78
+ queue_name: job.queue_name,
79
+ executions: job.executions,
80
+ provider_job_id: job.provider_job_id
81
+ }
82
+
83
+ # Add arguments if the job class allows it
84
+ job_context[:arguments] = job.arguments if job.class.log_arguments?
85
+
86
+ # Create exception log with job source and context
87
+ log_data = Log::Error.from_exception(
88
+ Source::Job,
89
+ exception,
90
+ job_context
91
+ )
92
+
93
+ # Use Rails logger with our structured formatter
94
+ logger.error(log_data)
95
+ end
96
+
97
+ sig { returns(::ActiveSupport::Logger) }
98
+ def logger
99
+ ::ActiveJob::Base.logger
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,38 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_job"
6
+ require "active_job/log_subscriber"
7
+ rescue LoadError
8
+ # ActiveJob gem is not available, integration will be skipped
9
+ end
10
+
11
+ require_relative "active_job/log_subscriber" if defined?(::ActiveJob::LogSubscriber)
12
+
13
+ module LogStruct
14
+ module Integrations
15
+ # ActiveJob integration for structured logging
16
+ module ActiveJob
17
+ extend T::Sig
18
+ extend IntegrationInterface
19
+
20
+ # Set up ActiveJob structured logging
21
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
22
+ def self.setup(config)
23
+ return nil unless defined?(::ActiveJob::LogSubscriber)
24
+ return nil unless config.enabled
25
+ return nil unless config.integrations.enable_activejob
26
+
27
+ ::ActiveSupport.on_load(:active_job) do
28
+ # Detach the default text formatter
29
+ ::ActiveJob::LogSubscriber.detach_from :active_job
30
+
31
+ # Attach our structured formatter
32
+ Integrations::ActiveJob::LogSubscriber.attach_to :active_job
33
+ end
34
+ true
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,258 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "active_support/notifications"
5
+
6
+ module LogStruct
7
+ module Integrations
8
+ # ActiveRecord Integration for SQL Query Logging
9
+ #
10
+ # This integration captures and structures all SQL queries executed through ActiveRecord,
11
+ # providing detailed performance and debugging information in a structured format.
12
+ #
13
+ # ## Features:
14
+ # - Captures all SQL queries with execution time
15
+ # - Safely filters sensitive data from bind parameters
16
+ # - Extracts database operation metadata
17
+ # - Provides connection pool monitoring information
18
+ # - Identifies query types and table names
19
+ #
20
+ # ## Performance Considerations:
21
+ # - Minimal overhead on query execution
22
+ # - Async logging prevents I/O blocking
23
+ # - Configurable to disable in production if needed
24
+ # - Smart filtering reduces log volume for repetitive queries
25
+ #
26
+ # ## Security:
27
+ # - SQL queries are always parameterized (safe)
28
+ # - Bind parameters filtered through LogStruct's param filters
29
+ # - Sensitive patterns automatically scrubbed
30
+ #
31
+ # ## Configuration:
32
+ # ```ruby
33
+ # LogStruct.configure do |config|
34
+ # config.integrations.enable_sql_logging = true
35
+ # config.integrations.sql_slow_query_threshold = 100.0 # ms
36
+ # config.integrations.sql_log_bind_params = false # disable in production
37
+ # end
38
+ # ```
39
+ module ActiveRecord
40
+ extend T::Sig
41
+ extend IntegrationInterface
42
+
43
+ # Set up SQL query logging integration
44
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
45
+ def self.setup(config)
46
+ return nil unless config.integrations.enable_sql_logging
47
+ return nil unless defined?(::ActiveRecord::Base)
48
+
49
+ subscribe_to_sql_notifications
50
+ true
51
+ end
52
+
53
+ private_class_method
54
+
55
+ # Subscribe to ActiveRecord's sql.active_record notifications
56
+ sig { void }
57
+ def self.subscribe_to_sql_notifications
58
+ ::ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
59
+ handle_sql_event(name, start, finish, id, payload)
60
+ rescue => error
61
+ LogStruct.handle_exception(error, source: LogStruct::Source::LogStruct)
62
+ end
63
+ end
64
+
65
+ # Process SQL notification event and create structured log
66
+ sig { params(name: String, start: T.untyped, finish: T.untyped, id: String, payload: T::Hash[Symbol, T.untyped]).void }
67
+ def self.handle_sql_event(name, start, finish, id, payload)
68
+ # Skip schema queries and Rails internal queries
69
+ return if skip_query?(payload)
70
+
71
+ duration = ((finish - start) * 1000.0).round(2)
72
+
73
+ # Skip fast queries if threshold is configured
74
+ config = LogStruct.config
75
+ if config.integrations.sql_slow_query_threshold&.positive?
76
+ return if duration < config.integrations.sql_slow_query_threshold
77
+ end
78
+
79
+ sql_log = Log::SQL.new(
80
+ message: format_sql_message(payload),
81
+ source: Source::App,
82
+ event: Event::Database,
83
+ sql: payload[:sql]&.strip || "",
84
+ name: payload[:name] || "SQL Query",
85
+ duration: duration,
86
+ row_count: extract_row_count(payload),
87
+ connection_adapter: extract_adapter_name(payload),
88
+ bind_params: extract_and_filter_binds(payload),
89
+ database_name: extract_database_name(payload),
90
+ connection_pool_size: extract_pool_size(payload),
91
+ active_connections: extract_active_connections(payload),
92
+ operation_type: extract_operation_type(payload),
93
+ table_names: extract_table_names(payload)
94
+ )
95
+
96
+ LogStruct.info(sql_log)
97
+ end
98
+
99
+ # Determine if query should be skipped from logging
100
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
101
+ def self.skip_query?(payload)
102
+ query_name = payload[:name]
103
+ sql = payload[:sql]
104
+
105
+ # Skip Rails schema queries
106
+ return true if query_name&.include?("SCHEMA")
107
+ return true if query_name&.include?("CACHE")
108
+
109
+ # Skip common Rails internal queries
110
+ return true if sql&.include?("schema_migrations")
111
+ return true if sql&.include?("ar_internal_metadata")
112
+
113
+ # Skip SHOW/DESCRIBE queries
114
+ return true if sql&.match?(/\A\s*(SHOW|DESCRIBE|EXPLAIN)\s/i)
115
+
116
+ false
117
+ end
118
+
119
+ # Format a readable message for the SQL log
120
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(String) }
121
+ def self.format_sql_message(payload)
122
+ operation_name = payload[:name] || "SQL Query"
123
+ "#{operation_name} executed"
124
+ end
125
+
126
+ # Extract row count from payload
127
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
128
+ def self.extract_row_count(payload)
129
+ row_count = payload[:row_count]
130
+ row_count.is_a?(Integer) ? row_count : nil
131
+ end
132
+
133
+ # Extract database adapter name
134
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
135
+ def self.extract_adapter_name(payload)
136
+ connection = payload[:connection]
137
+ return nil unless connection
138
+
139
+ adapter_name = connection.class.name
140
+ adapter_name&.split("::")&.last
141
+ end
142
+
143
+ # Extract and filter bind parameters
144
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[T.untyped])) }
145
+ def self.extract_and_filter_binds(payload)
146
+ return nil unless LogStruct.config.integrations.sql_log_bind_params
147
+
148
+ # Prefer type_casted_binds as they're more readable
149
+ binds = payload[:type_casted_binds] || payload[:binds]
150
+ return nil unless binds
151
+
152
+ # Filter sensitive data from bind parameters
153
+ binds.map do |bind|
154
+ filter_bind_parameter(bind)
155
+ end
156
+ end
157
+
158
+ # Extract database name from connection
159
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
160
+ def self.extract_database_name(payload)
161
+ connection = payload[:connection]
162
+ return nil unless connection
163
+
164
+ if connection.respond_to?(:current_database)
165
+ connection.current_database
166
+ elsif connection.respond_to?(:database)
167
+ connection.database
168
+ end
169
+ rescue
170
+ nil
171
+ end
172
+
173
+ # Extract connection pool size
174
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
175
+ def self.extract_pool_size(payload)
176
+ connection = payload[:connection]
177
+ return nil unless connection
178
+
179
+ pool = connection.pool if connection.respond_to?(:pool)
180
+ pool&.size
181
+ rescue
182
+ nil
183
+ end
184
+
185
+ # Extract active connection count
186
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
187
+ def self.extract_active_connections(payload)
188
+ connection = payload[:connection]
189
+ return nil unless connection
190
+
191
+ pool = connection.pool if connection.respond_to?(:pool)
192
+ pool&.stat&.[](:busy)
193
+ rescue
194
+ nil
195
+ end
196
+
197
+ # Extract SQL operation type (SELECT, INSERT, etc.)
198
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
199
+ def self.extract_operation_type(payload)
200
+ sql = payload[:sql]
201
+ return nil unless sql
202
+
203
+ # Extract first word of SQL query
204
+ match = sql.strip.match(/\A\s*(\w+)/i)
205
+ match&.captures&.first&.upcase
206
+ end
207
+
208
+ # Extract table names from SQL query
209
+ sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[String])) }
210
+ def self.extract_table_names(payload)
211
+ sql = payload[:sql]
212
+ return nil unless sql
213
+
214
+ # Simple regex to extract table names (basic implementation)
215
+ # This covers most common cases but could be enhanced
216
+ tables = []
217
+
218
+ # Match FROM, JOIN, UPDATE, INSERT INTO, DELETE FROM patterns
219
+ sql.scan(/(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM)\s+["`]?(\w+)["`]?/i) do |match|
220
+ table_name = match[0]
221
+ tables << table_name unless tables.include?(table_name)
222
+ end
223
+
224
+ tables.empty? ? nil : tables
225
+ end
226
+
227
+ # Filter individual bind parameter values to remove sensitive data
228
+ sig { params(value: T.untyped).returns(T.untyped) }
229
+ def self.filter_bind_parameter(value)
230
+ case value
231
+ when String
232
+ # Filter strings that look like passwords, tokens, secrets, etc.
233
+ if looks_sensitive?(value)
234
+ "[FILTERED]"
235
+ else
236
+ value
237
+ end
238
+ else
239
+ value
240
+ end
241
+ end
242
+
243
+ # Check if a string value looks sensitive and should be filtered
244
+ sig { params(value: String).returns(T::Boolean) }
245
+ def self.looks_sensitive?(value)
246
+ # Filter very long strings that might be tokens
247
+ return true if value.length > 50
248
+
249
+ # Filter strings that look like hashed passwords, API keys, tokens
250
+ return true if value.match?(/\A[a-f0-9]{32,}\z/i) # MD5, SHA, etc.
251
+ return true if value.match?(/\A[A-Za-z0-9+\/]{20,}={0,2}\z/) # Base64
252
+ return true if value.match?(/(password|secret|token|key|auth)/i)
253
+
254
+ false
255
+ end
256
+ end
257
+ end
258
+ end