logstruct 0.0.1 → 0.0.2.pre.rc1

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,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module Integrations
6
+ # Interface that all integrations must implement
7
+ # This ensures consistent behavior across all integration modules
8
+ module IntegrationInterface
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ # This is an interface that should be implemented by all integration modules
13
+ interface!
14
+
15
+ # All integrations must implement this method to set up their functionality
16
+ # @return [Boolean, nil] Returns true if setup was successful, nil if skipped
17
+ sig { abstract.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
18
+ def setup(config); end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,114 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "lograge"
6
+ rescue LoadError
7
+ # Lograge gem is not available, integration will be skipped
8
+ end
9
+
10
+ module LogStruct
11
+ module Integrations
12
+ # Lograge integration for structured request logging
13
+ module Lograge
14
+ extend IntegrationInterface
15
+
16
+ class << self
17
+ extend T::Sig
18
+
19
+ # Set up lograge for structured request logging
20
+ sig { override.params(logstruct_config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
21
+ def setup(logstruct_config)
22
+ return nil unless defined?(::Lograge)
23
+ return nil unless logstruct_config.enabled
24
+ return nil unless logstruct_config.integrations.enable_lograge
25
+
26
+ configure_lograge(logstruct_config)
27
+
28
+ true
29
+ end
30
+
31
+ private_class_method
32
+
33
+ sig { params(logstruct_config: LogStruct::Configuration).void }
34
+ def configure_lograge(logstruct_config)
35
+ ::Rails.application.configure do
36
+ config.lograge.enabled = true
37
+ # Use a raw formatter that just returns the log struct.
38
+ # The struct is converted to JSON by our Formatter (after filtering, etc.)
39
+ config.lograge.formatter = T.let(
40
+ lambda do |data|
41
+ # Convert the data hash to a Log::Request struct
42
+ Log::Request.new(
43
+ source: Source::Rails,
44
+ event: Event::Request,
45
+ timestamp: Time.now,
46
+ http_method: data[:method],
47
+ path: data[:path],
48
+ format: data[:format],
49
+ controller: data[:controller],
50
+ action: data[:action],
51
+ status: data[:status],
52
+ duration: data[:duration],
53
+ view: data[:view],
54
+ db: data[:db],
55
+ params: data[:params]
56
+ )
57
+ end,
58
+ T.proc.params(hash: T::Hash[Symbol, T.untyped]).returns(Log::Request)
59
+ )
60
+
61
+ # Add custom options to lograge
62
+ config.lograge.custom_options = lambda do |event|
63
+ Integrations::Lograge.lograge_default_options(event)
64
+ end
65
+ end
66
+ end
67
+
68
+ sig { params(event: ActiveSupport::Notifications::Event).returns(T::Hash[Symbol, T.untyped]) }
69
+ def lograge_default_options(event)
70
+ # Extract essential fields from the payload
71
+ options = event.payload.slice(
72
+ :request_id,
73
+ :host,
74
+ :source_ip
75
+ ).compact
76
+
77
+ if event.payload[:params].present?
78
+ options[:params] = event.payload[:params].except("controller", "action")
79
+ end
80
+
81
+ # Process headers if available
82
+ process_headers(event, options)
83
+
84
+ # Apply custom options from application if provided
85
+ apply_custom_options(event, options)
86
+
87
+ options
88
+ end
89
+
90
+ # Process headers from the event payload
91
+ sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
92
+ def process_headers(event, options)
93
+ headers = event.payload[:headers]
94
+ return if headers.blank?
95
+
96
+ options[:user_agent] = headers["HTTP_USER_AGENT"]
97
+ options[:content_type] = headers["CONTENT_TYPE"]
98
+ options[:accept] = headers["HTTP_ACCEPT"]
99
+ end
100
+
101
+ # Apply custom options from the application's configuration
102
+ sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
103
+ def apply_custom_options(event, options)
104
+ custom_options_proc = LogStruct.config.integrations.lograge_custom_options
105
+ return unless custom_options_proc&.respond_to?(:call)
106
+
107
+ # Call the proc with the event and options
108
+ # The proc can modify the options hash directly
109
+ custom_options_proc.call(event, options)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,31 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rack"
5
+ require "action_dispatch/middleware/show_exceptions"
6
+ require_relative "rack/error_handling_middleware"
7
+
8
+ module LogStruct
9
+ module Integrations
10
+ # Rack middleware integration for structured logging
11
+ module Rack
12
+ extend T::Sig
13
+ extend IntegrationInterface
14
+
15
+ # Set up Rack middleware for structured error logging
16
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
17
+ def self.setup(config)
18
+ return nil unless config.enabled
19
+ return nil unless config.integrations.enable_rack_error_handler
20
+
21
+ # Add structured logging middleware for security violations and errors
22
+ # Need to insert after ShowExceptions to catch IP spoofing errors
23
+ ::Rails.application.middleware.insert_after(
24
+ ::ActionDispatch::ShowExceptions,
25
+ Integrations::RackErrorHandler::Middleware
26
+ )
27
+ true
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,146 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module Integrations
6
+ module RackErrorHandler
7
+ # Custom middleware to enhance Rails error logging with JSON format and request details
8
+ class Middleware
9
+ extend T::Sig
10
+
11
+ # IP Spoofing error response
12
+ IP_SPOOF_HTML = T.let(
13
+ "<html><head><title>IP Spoofing Detected</title></head><body>" \
14
+ "<h1>Forbidden</h1>" \
15
+ "<p>IP spoofing detected. This request has been blocked for security reasons.</p>" \
16
+ "</body></html>",
17
+ String
18
+ )
19
+
20
+ # CSRF error response
21
+ CSRF_HTML = T.let(
22
+ "<html><head><title>CSRF Error</title></head><body>" \
23
+ "<h1>Forbidden</h1>" \
24
+ "<p>Invalid authenticity token. This request has been blocked to prevent cross-site request forgery.</p>" \
25
+ "</body></html>",
26
+ String
27
+ )
28
+
29
+ # Response headers calculated at load time
30
+ IP_SPOOF_HEADERS = T.let(
31
+ {
32
+ "Content-Type" => "text/html",
33
+ "Content-Length" => IP_SPOOF_HTML.bytesize.to_s
34
+ }.freeze,
35
+ T::Hash[String, String]
36
+ )
37
+
38
+ CSRF_HEADERS = T.let(
39
+ {
40
+ "Content-Type" => "text/html",
41
+ "Content-Length" => CSRF_HTML.bytesize.to_s
42
+ }.freeze,
43
+ T::Hash[String, String]
44
+ )
45
+
46
+ # HTTP status code for forbidden responses
47
+ FORBIDDEN_STATUS = T.let(403, Integer)
48
+
49
+ sig { params(app: T.untyped).void }
50
+ def initialize(app)
51
+ @app = app
52
+ end
53
+
54
+ sig { params(env: T.untyped).returns(T.untyped) }
55
+ def call(env)
56
+ return @app.call(env) unless LogStruct.enabled?
57
+
58
+ # Try to process the request
59
+ begin
60
+ @app.call(env)
61
+ rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
62
+ # Create a security log for IP spoofing
63
+ security_log = Log::Security.new(
64
+ event: Event::IPSpoof,
65
+ message: ip_spoof_error.message,
66
+ # Can't call .remote_ip on the request because that's what raises the error.
67
+ # Have to pass the client_ip and x_forwarded_for headers.
68
+ client_ip: env["HTTP_CLIENT_IP"],
69
+ x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
70
+ path: env["PATH_INFO"],
71
+ http_method: env["REQUEST_METHOD"],
72
+ user_agent: env["HTTP_USER_AGENT"],
73
+ referer: env["HTTP_REFERER"],
74
+ request_id: env["action_dispatch.request_id"]
75
+ )
76
+
77
+ # Log the structured data
78
+ ::Rails.logger.warn(security_log)
79
+
80
+ # Report the error
81
+ context = extract_request_context(env)
82
+ LogStruct.handle_exception(ip_spoof_error, source: Source::Security, context: context)
83
+
84
+ # If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
85
+ # If we are only logging or reporting these security errors, then return a default response
86
+ [FORBIDDEN_STATUS, IP_SPOOF_HEADERS, [IP_SPOOF_HTML]]
87
+ rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
88
+ # Create a security log for CSRF error
89
+ request = ::ActionDispatch::Request.new(env)
90
+ security_log = Log::Security.new(
91
+ event: Event::CSRFViolation,
92
+ message: invalid_auth_token_error.message,
93
+ path: request.path,
94
+ http_method: request.method,
95
+ source_ip: request.remote_ip,
96
+ user_agent: request.user_agent,
97
+ referer: request.referer,
98
+ request_id: request.request_id
99
+ )
100
+ LogStruct.error(security_log)
101
+
102
+ # Report to error reporting service and/or re-raise
103
+ context = extract_request_context(env)
104
+ LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
105
+
106
+ # If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
107
+ # If we are only logging or reporting these security errors, then return a default response
108
+ [FORBIDDEN_STATUS, CSRF_HEADERS, [CSRF_HTML]]
109
+ rescue => error
110
+ # Extract request context for error reporting
111
+ context = extract_request_context(env)
112
+
113
+ # Create and log a structured exception with request context
114
+ exception_log = Log::Error.from_exception(
115
+ Source::Rails,
116
+ error,
117
+ context
118
+ )
119
+ LogStruct.error(exception_log)
120
+
121
+ # Re-raise any standard errors to let Rails or error reporter handle it.
122
+ # Rails will also log the request details separately
123
+ raise error
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ sig { params(env: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
130
+ def extract_request_context(env)
131
+ request = ::ActionDispatch::Request.new(env)
132
+ {
133
+ request_id: request.request_id,
134
+ path: request.path,
135
+ method: request.method,
136
+ user_agent: request.user_agent,
137
+ referer: request.referer
138
+ }
139
+ rescue => error
140
+ # If we can't extract request context, return minimal info
141
+ {error_extracting_context: error.message}
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rack"
5
+ require "action_dispatch/middleware/show_exceptions"
6
+ require_relative "rack_error_handler/middleware"
7
+
8
+ module LogStruct
9
+ module Integrations
10
+ # Rack middleware integration for structured logging
11
+ module RackErrorHandler
12
+ extend T::Sig
13
+ extend IntegrationInterface
14
+
15
+ # Set up Rack middleware for structured error logging
16
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
17
+ def self.setup(config)
18
+ return nil unless config.enabled
19
+ return nil unless config.integrations.enable_rack_error_handler
20
+
21
+ # Add structured logging middleware for security violations and errors
22
+ # Need to insert after ShowExceptions to catch IP spoofing errors
23
+ ::Rails.application.middleware.insert_after(
24
+ ::ActionDispatch::ShowExceptions,
25
+ Integrations::RackErrorHandler::Middleware
26
+ )
27
+
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,75 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "shrine"
6
+ rescue LoadError
7
+ # Shrine gem is not available, integration will be skipped
8
+ end
9
+
10
+ module LogStruct
11
+ module Integrations
12
+ # Shrine integration for structured logging
13
+ module Shrine
14
+ extend T::Sig
15
+ extend IntegrationInterface
16
+
17
+ # Set up Shrine structured logging
18
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
19
+ def self.setup(config)
20
+ return nil unless defined?(::Shrine)
21
+ return nil unless config.enabled
22
+ return nil unless config.integrations.enable_shrine
23
+
24
+ # Create a structured log subscriber for Shrine
25
+ # ActiveSupport::Notifications::Event has name, time, end, transaction_id, payload, and duration
26
+ shrine_log_subscriber = T.unsafe(lambda do |event|
27
+ payload = event.payload.except(:io, :metadata, :name).dup
28
+
29
+ # Map event name to Event type
30
+ event_type = case event.name
31
+ when :upload then Event::Upload
32
+ when :download then Event::Download
33
+ when :delete then Event::Delete
34
+ when :metadata then Event::Metadata
35
+ when :exists then Event::Exist # ActiveStorage uses 'exist', may as well use that
36
+ else Event::Unknown
37
+ end
38
+
39
+ # Create structured log data
40
+ log_data = Log::Shrine.new(
41
+ source: Source::Shrine,
42
+ event: event_type,
43
+ duration: event.duration,
44
+ storage: payload[:storage],
45
+ location: payload[:location],
46
+ uploader: payload[:uploader],
47
+ upload_options: payload[:upload_options],
48
+ download_options: payload[:download_options],
49
+ options: payload[:options],
50
+ # Data is flattened by the JSON formatter
51
+ additional_data: payload.except(
52
+ :storage,
53
+ :location,
54
+ :uploader,
55
+ :upload_options,
56
+ :download_options,
57
+ :options
58
+ )
59
+ )
60
+
61
+ # Pass the structured hash to the logger
62
+ # If Rails.logger has our Formatter, it will handle JSON conversion
63
+ ::Shrine.logger.info log_data
64
+ end)
65
+
66
+ # Configure Shrine to use our structured log subscriber
67
+ ::Shrine.plugin :instrumentation,
68
+ events: %i[upload exists download delete],
69
+ log_subscriber: shrine_log_subscriber
70
+
71
+ true
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../semantic_logger/logger"
5
+ require_relative "../../log/sidekiq"
6
+ require_relative "../../enums/source"
7
+
8
+ module LogStruct
9
+ module Integrations
10
+ module Sidekiq
11
+ extend T::Sig
12
+
13
+ # Get thread ID for Sidekiq logging
14
+ sig { returns(String) }
15
+ def self.tid
16
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
17
+ end
18
+
19
+ # Custom Logger for Sidekiq that creates LogStruct::Log::Sidekiq entries
20
+ class Logger < LogStruct::SemanticLogger::Logger
21
+ extend T::Sig
22
+
23
+ # Override log methods to create Sidekiq log structs
24
+ %i[debug info warn error fatal].each do |level|
25
+ define_method(level) do |message = nil, payload = nil, &block|
26
+ # Create a Sidekiq log struct with the message
27
+ log_struct = Log::Sidekiq.new(
28
+ level: LogStruct::Level.from_severity(level.to_s.upcase),
29
+ event: Event::Log,
30
+ message: message || (block ? block.call : ""),
31
+ process_id: ::Process.pid,
32
+ thread_id: Sidekiq.tid,
33
+ context: ::Sidekiq::Context.current || {}
34
+ )
35
+
36
+ # Pass the struct to SemanticLogger
37
+ super(log_struct, payload, &nil)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "sidekiq"
6
+ rescue LoadError
7
+ # Sidekiq gem is not available, integration will be skipped
8
+ end
9
+ require_relative "sidekiq/logger" if defined?(::Sidekiq)
10
+
11
+ module LogStruct
12
+ module Integrations
13
+ # Sidekiq integration for structured logging
14
+ module Sidekiq
15
+ extend T::Sig
16
+ extend IntegrationInterface
17
+
18
+ # Set up Sidekiq structured logging
19
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
20
+ def self.setup(config)
21
+ return nil unless defined?(::Sidekiq)
22
+ return nil unless config.enabled
23
+ return nil unless config.integrations.enable_sidekiq
24
+
25
+ # Configure Sidekiq server (worker) to use our logger
26
+ ::Sidekiq.configure_server do |sidekiq_config|
27
+ sidekiq_config.logger = LogStruct::Integrations::Sidekiq::Logger.new("Sidekiq-Server")
28
+ end
29
+
30
+ # Configure Sidekiq client (Rails app) to use our logger
31
+ ::Sidekiq.configure_client do |sidekiq_config|
32
+ sidekiq_config.logger = LogStruct::Integrations::Sidekiq::Logger.new("Sidekiq-Client")
33
+ end
34
+
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module LogStruct
7
+ module Integrations
8
+ # Integration for Sorbet runtime type checking error handlers
9
+ # This module installs error handlers that report type errors through LogStruct
10
+ # These handlers can be enabled/disabled using configuration
11
+ module Sorbet
12
+ extend T::Sig
13
+ extend IntegrationInterface
14
+
15
+ # Set up Sorbet error handlers to report errors through LogStruct
16
+ sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
17
+ def self.setup(config)
18
+ return nil unless config.integrations.enable_sorbet_error_handlers
19
+
20
+ # Install inline type error handler
21
+ # Called when T.let, T.cast, T.must, etc. fail
22
+ T::Configuration.inline_type_error_handler = lambda do |error, _opts|
23
+ LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
24
+ end
25
+
26
+ # Install call validation error handler
27
+ # Called when method signature validation fails
28
+ T::Configuration.call_validation_error_handler = lambda do |_signature, opts|
29
+ error = TypeError.new(opts[:pretty_message])
30
+ LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
31
+ end
32
+
33
+ # Install sig builder error handler
34
+ # Called when there's a problem with a signature definition
35
+ T::Configuration.sig_builder_error_handler = lambda do |error, _location|
36
+ LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
37
+ end
38
+
39
+ # Install sig validation error handler
40
+ # Called when there's a problem with a signature validation
41
+ T::Configuration.sig_validation_error_handler = lambda do |error, _opts|
42
+ LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
43
+ end
44
+
45
+ true
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "integrations/integration_interface"
5
+ require_relative "integrations/active_job"
6
+ require_relative "integrations/active_record"
7
+ require_relative "integrations/rack_error_handler"
8
+ require_relative "integrations/host_authorization"
9
+ require_relative "integrations/action_mailer"
10
+ require_relative "integrations/lograge"
11
+ require_relative "integrations/shrine"
12
+ require_relative "integrations/sidekiq"
13
+ require_relative "integrations/good_job"
14
+ require_relative "integrations/active_storage"
15
+ require_relative "integrations/carrierwave"
16
+ require_relative "integrations/sorbet"
17
+
18
+ module LogStruct
19
+ module Integrations
20
+ extend T::Sig
21
+
22
+ sig { void }
23
+ def self.setup_integrations
24
+ config = LogStruct.config
25
+
26
+ # Set up each integration with consistent configuration pattern
27
+ Integrations::Lograge.setup(config) if config.integrations.enable_lograge
28
+ Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
29
+ Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
30
+ Integrations::ActiveRecord.setup(config) if config.integrations.enable_sql_logging
31
+ Integrations::Sidekiq.setup(config) if config.integrations.enable_sidekiq
32
+ Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
33
+ Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
34
+ Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
35
+ Integrations::Shrine.setup(config) if config.integrations.enable_shrine
36
+ Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
37
+ Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
38
+ Integrations::Sorbet.setup(config) if config.integrations.enable_sorbet_error_handlers
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "interfaces/common_fields"
5
+ require_relative "interfaces/additional_data_field"
6
+ require_relative "shared/serialize_common"
7
+ require_relative "shared/merge_additional_data_fields"
8
+ require_relative "../enums/source"
9
+ require_relative "../enums/event"
10
+ require_relative "../enums/level"
11
+ require_relative "../log_keys"
12
+
13
+ module LogStruct
14
+ module Log
15
+ # Email log entry for structured logging
16
+ class ActionMailer < T::Struct
17
+ extend T::Sig
18
+
19
+ include Interfaces::CommonFields
20
+ include Interfaces::AdditionalDataField
21
+ include SerializeCommon
22
+ include MergeAdditionalDataFields
23
+
24
+ ActionMailerEvent = T.type_alias {
25
+ T.any(Event::Delivery, Event::Delivered)
26
+ }
27
+
28
+ # Common fields
29
+ const :source, Source::Mailer, default: T.let(Source::Mailer, Source::Mailer)
30
+ const :event, ActionMailerEvent
31
+ const :timestamp, Time, factory: -> { Time.now }
32
+ const :level, Level, default: T.let(Level::Info, Level)
33
+
34
+ # Email-specific fields
35
+ const :to, T.nilable(T.any(String, T::Array[String])), default: nil
36
+ const :from, T.nilable(String), default: nil
37
+ const :subject, T.nilable(String), default: nil
38
+ const :additional_data, T::Hash[Symbol, T.untyped], default: {}
39
+
40
+ # Convert the log entry to a hash for serialization
41
+ sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
42
+ def serialize(strict = true)
43
+ hash = serialize_common(strict)
44
+ merge_additional_data_fields(hash)
45
+
46
+ # Add email-specific fields if they're present
47
+ hash[LOG_KEYS.fetch(:to)] = to if to
48
+ hash[LOG_KEYS.fetch(:from)] = from if from
49
+ hash[LOG_KEYS.fetch(:subject)] = subject if subject
50
+
51
+ hash
52
+ end
53
+ end
54
+ end
55
+ end