loading
Generated 2026-10-18T03:35:18+00:00

All Files ( 92.84% covered at 196.42 hits/line )

163 files in total.
5883 relevant lines, 5462 lines covered and 421 lines missed. ( 92.84% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/log_struct.rb 86.84 % 74 38 33 5 3.39
lib/log_struct/boot_buffer.rb 100.00 % 28 15 15 0 3.07
lib/log_struct/concerns/configuration.rb 92.39 % 325 184 170 14 365.98
lib/log_struct/concerns/error_handling.rb 91.67 % 120 48 44 4 10.98
lib/log_struct/concerns/logging.rb 100.00 % 45 21 21 0 8.33
lib/log_struct/config_struct/error_handling_modes.rb 100.00 % 27 8 8 0 2.00
lib/log_struct/config_struct/filters.rb 98.36 % 202 61 60 1 850.26
lib/log_struct/config_struct/integrations.rb 100.00 % 94 25 25 0 9.92
lib/log_struct/config_struct/limits.rb 100.00 % 29 8 8 0 2.00
lib/log_struct/configuration.rb 100.00 % 74 30 30 0 2094.67
lib/log_struct/enums.rb 100.00 % 9 5 5 0 2.00
lib/log_struct/enums/error_handling_mode.rb 100.00 % 22 9 9 0 2.00
lib/log_struct/enums/error_reporter.rb 100.00 % 14 8 8 0 2.00
lib/log_struct/enums/event.rb 100.00 % 63 32 32 0 2.00
lib/log_struct/enums/level.rb 100.00 % 66 43 43 0 207.07
lib/log_struct/enums/log_field.rb 100.00 % 173 117 117 0 2.00
lib/log_struct/enums/source.rb 100.00 % 29 16 16 0 2.00
lib/log_struct/formatter.rb 96.80 % 488 219 212 7 1678.34
lib/log_struct/handlers.rb 100.00 % 27 7 7 0 2.29
lib/log_struct/hash_utils.rb 95.65 % 50 23 22 1 47.96
lib/log_struct/integrations.rb 95.00 % 89 60 57 3 2.10
lib/log_struct/integrations/action_mailer.rb 90.00 % 62 30 27 3 4.40
lib/log_struct/integrations/action_mailer/error_handling.rb 88.03 % 287 117 103 14 14.05
lib/log_struct/integrations/action_mailer/event_logging.rb 92.16 % 116 51 47 4 20.20
lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb 100.00 % 81 42 42 0 2.17
lib/log_struct/integrations/action_mailer/metadata_collection.rb 100.00 % 46 22 22 0 43.32
lib/log_struct/integrations/active_job.rb 100.00 % 38 17 17 0 3.12
lib/log_struct/integrations/active_job/log_subscriber.rb 100.00 % 260 126 126 0 7.60
lib/log_struct/integrations/active_model_serializers.rb 100.00 % 55 20 20 0 2.40
lib/log_struct/integrations/active_record.rb 93.48 % 316 138 129 9 15.91
lib/log_struct/integrations/active_storage.rb 45.10 % 133 51 23 28 0.82
lib/log_struct/integrations/ahoy.rb 100.00 % 57 25 25 0 3.36
lib/log_struct/integrations/carrierwave.rb 100.00 % 110 47 47 0 3.49
lib/log_struct/integrations/dotenv.rb 74.83 % 264 151 113 38 1.56
lib/log_struct/integrations/event_time.rb 100.00 % 19 10 10 0 18.50
lib/log_struct/integrations/good_job.rb 100.00 % 180 70 70 0 4.07
lib/log_struct/integrations/good_job/logger.rb 100.00 % 73 24 24 0 4.42
lib/log_struct/integrations/host_authorization.rb 97.78 % 103 45 44 1 3.64
lib/log_struct/integrations/integration_interface.rb 100.00 % 21 8 8 0 2.25
lib/log_struct/integrations/lograge.rb 97.22 % 264 108 105 3 5.18
lib/log_struct/integrations/puma.rb 61.45 % 532 262 161 101 1.89
lib/log_struct/integrations/rack_error_handler.rb 100.00 % 48 23 23 0 2.09
lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb 68.75 % 35 16 11 5 1.38
lib/log_struct/integrations/rack_error_handler/error_lines.rb 89.83 % 160 59 53 6 2.69
lib/log_struct/integrations/rack_error_handler/middleware.rb 91.67 % 28 12 11 1 1.92
lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb 100.00 % 36 14 14 0 1.79
lib/log_struct/integrations/rack_middleware.rb 100.00 % 23 11 11 0 2.55
lib/log_struct/integrations/rack_setup.rb 93.33 % 28 15 14 1 2.13
lib/log_struct/integrations/rails_log_subscribers.rb 100.00 % 32 14 14 0 4.71
lib/log_struct/integrations/request_context.rb 100.00 % 27 11 11 0 2.18
lib/log_struct/integrations/request_context/middleware.rb 50.00 % 26 14 7 7 1.00
lib/log_struct/integrations/shrine.rb 50.00 % 135 52 26 26 1.23
lib/log_struct/integrations/sidekiq.rb 91.53 % 130 59 54 5 2.25
lib/log_struct/integrations/sidekiq/job_context_middleware.rb 100.00 % 32 10 10 0 1.20
lib/log_struct/integrations/sidekiq/logger.rb 100.00 % 58 23 23 0 2.13
lib/log_struct/integrations/sorbet.rb 100.00 % 61 27 27 0 10.11
lib/log_struct/log.rb 100.00 % 44 16 16 0 5.00
lib/log_struct/log/action_mailer.rb 100.00 % 53 20 20 0 4.45
lib/log_struct/log/action_mailer/delivered.rb 100.00 % 64 42 42 0 2.50
lib/log_struct/log/action_mailer/delivery.rb 100.00 % 64 42 42 0 10.64
lib/log_struct/log/action_mailer/error.rb 100.00 % 72 48 48 0 15.35
lib/log_struct/log/active_job.rb 100.00 % 55 24 24 0 4.25
lib/log_struct/log/active_job/discard.rb 100.00 % 63 41 41 0 2.32
lib/log_struct/log/active_job/enqueue.rb 100.00 % 57 37 37 0 4.46
lib/log_struct/log/active_job/error.rb 100.00 % 65 43 43 0 5.42
lib/log_struct/log/active_job/finish.rb 100.00 % 63 41 41 0 5.68
lib/log_struct/log/active_job/retry.rb 100.00 % 65 43 43 0 2.33
lib/log_struct/log/active_job/retry_stopped.rb 100.00 % 63 41 41 0 2.32
lib/log_struct/log/active_job/schedule.rb 100.00 % 61 39 39 0 2.31
lib/log_struct/log/active_job/start.rb 100.00 % 63 41 41 0 5.49
lib/log_struct/log/active_model_serializers.rb 100.00 % 54 34 34 0 2.29
lib/log_struct/log/active_storage.rb 88.89 % 41 18 16 2 1.78
lib/log_struct/log/active_storage/delete.rb 100.00 % 53 31 31 0 2.10
lib/log_struct/log/active_storage/download.rb 100.00 % 55 33 33 0 2.09
lib/log_struct/log/active_storage/exist.rb 100.00 % 53 31 31 0 2.10
lib/log_struct/log/active_storage/metadata.rb 100.00 % 53 31 31 0 2.10
lib/log_struct/log/active_storage/upload.rb 100.00 % 55 33 33 0 2.09
lib/log_struct/log/active_storage/url.rb 100.00 % 49 29 29 0 2.10
lib/log_struct/log/ahoy.rb 100.00 % 50 30 30 0 2.57
lib/log_struct/log/carrierwave.rb 90.48 % 56 21 19 2 1.81
lib/log_struct/log/carrierwave/delete.rb 100.00 % 61 41 41 0 2.34
lib/log_struct/log/carrierwave/download.rb 100.00 % 69 47 47 0 2.36
lib/log_struct/log/carrierwave/upload.rb 100.00 % 73 51 51 0 3.31
lib/log_struct/log/dotenv.rb 100.00 % 12 4 4 0 2.00
lib/log_struct/log/dotenv/load.rb 100.00 % 48 27 27 0 2.37
lib/log_struct/log/dotenv/restore.rb 100.00 % 48 27 27 0 2.11
lib/log_struct/log/dotenv/save.rb 100.00 % 48 27 27 0 2.37
lib/log_struct/log/dotenv/update.rb 100.00 % 48 27 27 0 2.37
lib/log_struct/log/error.rb 100.00 % 78 53 53 0 11.34
lib/log_struct/log/good_job.rb 88.24 % 46 17 15 2 1.76
lib/log_struct/log/good_job/error.rb 100.00 % 71 49 49 0 3.08
lib/log_struct/log/good_job/log.rb 100.00 % 67 45 45 0 6.51
lib/log_struct/log/interfaces/public_common_fields.rb 100.00 % 4 1 1 0 2.00
lib/log_struct/log/plain.rb 100.00 % 51 29 29 0 146.00
lib/log_struct/log/puma.rb 100.00 % 11 3 3 0 2.00
lib/log_struct/log/puma/error.rb 100.00 % 63 40 40 0 2.53
lib/log_struct/log/puma/shutdown.rb 100.00 % 53 30 30 0 2.10
lib/log_struct/log/puma/start.rb 100.00 % 69 46 46 0 2.07
lib/log_struct/log/request.rb 100.00 % 86 61 61 0 3.87
lib/log_struct/log/security.rb 88.89 % 47 18 16 2 1.78
lib/log_struct/log/security/blocked_host.rb 100.00 % 74 48 48 0 2.38
lib/log_struct/log/security/csrf_violation.rb 100.00 % 68 42 42 0 2.45
lib/log_struct/log/security/ip_spoof.rb 100.00 % 72 46 46 0 2.07
lib/log_struct/log/shrine.rb 100.00 % 13 5 5 0 2.00
lib/log_struct/log/shrine/delete.rb 100.00 % 50 29 29 0 2.10
lib/log_struct/log/shrine/download.rb 100.00 % 52 31 31 0 2.10
lib/log_struct/log/shrine/exist.rb 100.00 % 50 29 29 0 2.10
lib/log_struct/log/shrine/metadata.rb 100.00 % 50 29 29 0 2.10
lib/log_struct/log/shrine/upload.rb 100.00 % 62 41 41 0 2.34
lib/log_struct/log/sidekiq.rb 100.00 % 10 2 2 0 2.00
lib/log_struct/log/sidekiq/error.rb 100.00 % 63 40 40 0 2.53
lib/log_struct/log/sidekiq/log.rb 100.00 % 59 36 36 0 2.67
lib/log_struct/log/sql.rb 100.00 % 73 51 51 0 5.84
lib/log_struct/logged_errors.rb 100.00 % 25 11 11 0 21.09
lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb 96.43 % 61 28 27 1 4.64
lib/log_struct/multi_error_reporter.rb 84.62 % 207 104 88 16 2.17
lib/log_struct/param_filters.rb 89.71 % 145 68 61 7 2208.99
lib/log_struct/params_errors.rb 100.00 % 78 38 38 0 26.18
lib/log_struct/rails_boot_banner_silencer.rb 93.44 % 121 61 57 4 2.10
lib/log_struct/railtie.rb 71.43 % 91 42 30 12 1.71
lib/log_struct/semantic_logger/color_formatter.rb 84.26 % 201 108 91 17 5.17
lib/log_struct/semantic_logger/concerns/log_methods.rb 100.00 % 108 63 63 0 244.16
lib/log_struct/semantic_logger/formatter.rb 94.94 % 165 79 75 4 474.51
lib/log_struct/semantic_logger/logger.rb 86.67 % 138 60 52 8 10.15
lib/log_struct/semantic_logger/reopen_after_fork.rb 89.47 % 40 19 17 2 2.32
lib/log_struct/semantic_logger/setup.rb 82.18 % 244 101 83 18 3.31
lib/log_struct/shared/add_request_fields.rb 68.75 % 27 16 11 5 1.38
lib/log_struct/shared/interfaces/additional_data_field.rb 100.00 % 22 10 10 0 2.00
lib/log_struct/shared/interfaces/common_field_base.rb 100.00 % 29 14 14 0 2.07
lib/log_struct/shared/interfaces/common_fields.rb 100.00 % 30 16 16 0 2.00
lib/log_struct/shared/interfaces/public_common_fields.rb 100.00 % 20 10 10 0 2.00
lib/log_struct/shared/interfaces/request_fields.rb 100.00 % 35 17 17 0 2.00
lib/log_struct/shared/merge_additional_data_fields.rb 100.00 % 40 21 21 0 161.57
lib/log_struct/shared/serialize_common.rb 100.00 % 59 29 29 0 867.76
lib/log_struct/shared/serialize_common_public.rb 95.65 % 44 23 22 1 3.00
lib/log_struct/sorbet.rb 100.00 % 13 2 2 0 2.00
lib/log_struct/sorbet/serialize_symbol_keys.rb 91.67 % 23 12 11 1 2.08
lib/log_struct/string_scrubber.rb 98.55 % 293 138 136 2 2443.46
lib/logstruct.rb 100.00 % 4 1 1 0 1.00
rails_test_app/logstruct_test_app/Rakefile 100.00 % 6 2 2 0 1.00
rails_test_app/logstruct_test_app/app/controllers/application_controller.rb 100.00 % 5 1 1 0 1.00
rails_test_app/logstruct_test_app/app/controllers/errors_controller.rb 100.00 % 11 4 4 0 2.00
rails_test_app/logstruct_test_app/app/controllers/logging_controller.rb 74.55 % 168 55 41 14 2.65
rails_test_app/logstruct_test_app/app/controllers/security_controller.rb 100.00 % 15 5 5 0 1.60
rails_test_app/logstruct_test_app/app/jobs/application_job.rb 100.00 % 5 1 1 0 1.00
rails_test_app/logstruct_test_app/app/jobs/test_job.rb 30.00 % 34 10 3 7 0.30
rails_test_app/logstruct_test_app/app/mailers/application_mailer.rb 100.00 % 4 3 3 0 1.00
rails_test_app/logstruct_test_app/app/mailers/test_mailer.rb 100.00 % 15 8 8 0 1.00
rails_test_app/logstruct_test_app/app/models/application_record.rb 100.00 % 7 3 3 0 1.00
rails_test_app/logstruct_test_app/app/models/document.rb 100.00 % 33 17 17 0 4.06
rails_test_app/logstruct_test_app/app/models/user.rb 81.82 % 23 11 9 2 0.91
rails_test_app/logstruct_test_app/app/serializers/user_serializer.rb 100.00 % 6 2 2 0 1.00
rails_test_app/logstruct_test_app/app/uploaders/document_uploader.rb 100.00 % 5 1 1 0 1.00
rails_test_app/logstruct_test_app/config/application.rb 100.00 % 45 16 16 0 2.13
rails_test_app/logstruct_test_app/config/environment.rb 100.00 % 5 2 2 0 1.00
rails_test_app/logstruct_test_app/config/environments/test.rb 100.00 % 61 14 14 0 1.00
rails_test_app/logstruct_test_app/config/initializers/cors.rb 100.00 % 16 0 0 0 0.00
rails_test_app/logstruct_test_app/config/initializers/filter_parameter_logging.rb 100.00 % 8 1 1 0 1.00
rails_test_app/logstruct_test_app/config/initializers/inflections.rb 100.00 % 16 0 0 0 0.00
rails_test_app/logstruct_test_app/config/initializers/logstruct.rb 92.31 % 44 26 24 2 0.92
rails_test_app/logstruct_test_app/config/initializers/shrine.rb 100.00 % 20 6 6 0 1.00
rails_test_app/logstruct_test_app/config/routes.rb 100.00 % 26 16 16 0 1.44
rails_test_app/logstruct_test_app/lib/tasks/logging.rake 50.00 % 12 6 3 3 0.50

lib/log_struct.rb

86.84% lines covered

38 relevant lines. 33 lines covered and 5 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # Core library files
  4. 2 require "log_struct/sorbet"
  5. 2 require "log_struct/version"
  6. 2 require "log_struct/enums"
  7. 2 require "log_struct/configuration"
  8. 2 require "log_struct/formatter"
  9. 2 require "log_struct/logged_errors"
  10. 2 require "log_struct/railtie"
  11. 2 require "log_struct/concerns/error_handling"
  12. 2 require "log_struct/concerns/configuration"
  13. 2 require "log_struct/concerns/logging"
  14. # Require integrations
  15. 2 require "log_struct/integrations"
  16. # SemanticLogger: the logger, formatters and setup LogStruct writes its lines with
  17. 2 require "log_struct/semantic_logger/formatter"
  18. 2 require "log_struct/semantic_logger/color_formatter"
  19. 2 require "log_struct/semantic_logger/logger"
  20. 2 require "log_struct/semantic_logger/setup"
  21. 2 require "log_struct/rails_boot_banner_silencer"
  22. # Monkey patches for Rails compatibility
  23. 2 require "log_struct/monkey_patches/active_support/tagged_logging/formatter"
  24. 2 module LogStruct
  25. 2 extend T::Sig
  26. 2 @server_mode = T.let(false, T::Boolean)
  27. 2 class Error < StandardError; end
  28. 2 extend Concerns::ErrorHandling::ClassMethods
  29. 2 extend Concerns::Configuration::ClassMethods
  30. 2 extend Concerns::Logging::ClassMethods
  31. 4 sig { returns(T::Boolean) }
  32. 2 def self.server_mode?
  33. 37 @server_mode
  34. end
  35. 3 sig { params(value: T::Boolean).void }
  36. 2 def self.server_mode=(value)
  37. 27 @server_mode = value
  38. end
  39. # Set enabled at require time based on current Rails environment.
  40. # (Users can override this in their initializer which runs before the Railtie checks enabled)
  41. 2 set_enabled_from_rails_env!
  42. # Silence Rails boot banners for cleaner server output
  43. 2 LogStruct::RailsBootBannerSilencer.install!
  44. # Patch Puma immediately for server runs so we can convert its lifecycle
  45. # messages into structured logs reliably. Not when LogStruct is disabled, and not
  46. # when Puma isn't installed (install_patches! skips it).
  47. 2 if LogStruct.enabled? && ARGV.include?("server")
  48. begin
  49. require "log_struct/integrations/puma"
  50. LogStruct::Integrations::Puma.install_patches!
  51. # Patches installed now; Rack handler patch covers server boot path
  52. rescue => e
  53. if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test?
  54. raise e
  55. else
  56. LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :puma})
  57. end
  58. end
  59. end
  60. end

lib/log_struct/boot_buffer.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Collects structured logs during very early boot before the logger is ready.
  5. 2 module BootBuffer
  6. 2 extend T::Sig
  7. 2 @@logs = T.let([], T::Array[LogStruct::Log::Interfaces::CommonFields])
  8. 4 sig { params(log: LogStruct::Log::Interfaces::CommonFields).void }
  9. 2 def self.add(log)
  10. 5 @@logs << log
  11. end
  12. 4 sig { void }
  13. 2 def self.flush
  14. 4 return if @@logs.empty?
  15. 5 @@logs.each { |l| LogStruct.info(l) }
  16. 2 @@logs.clear
  17. end
  18. 3 sig { void }
  19. 2 def self.clear
  20. 5 @@logs.clear
  21. end
  22. end
  23. end

lib/log_struct/concerns/configuration.rb

92.39% lines covered

184 relevant lines. 170 lines covered and 14 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/parameter_filter"
  4. 2 require "active_support/core_ext/object/duplicable"
  5. 2 require_relative "../configuration"
  6. 2 module LogStruct
  7. 2 module Concerns
  8. # LogStruct.configure, whether LogStruct is on in this process, and merging Rails' filter_parameters
  9. 2 module Configuration
  10. 2 module ClassMethods
  11. 2 extend T::Sig
  12. 2 CONSOLE_COMMAND_ARGS = T.let(["console", "c"].freeze, T::Array[String])
  13. 2 EMPTY_ARGV = T.let([].freeze, T::Array[String])
  14. 2 RAILS_PATTERNS_LABEL = "Rails filter_parameters"
  15. 2 RAILS_REGEXPS_LABEL = "Rails filter_parameters (Regexps)"
  16. 2 RAILS_PATHS_LABEL = "Rails filter_parameters (paths)"
  17. 2 RAILS_PATTERN_LABELS = T.let([RAILS_PATTERNS_LABEL, RAILS_REGEXPS_LABEL].freeze, T::Array[String])
  18. 2 RAILS_FILTER_MASK = "[FILTERED]"
  19. 2 RAILS_FILTER_PROBE = "value"
  20. 4 sig { params(block: T.proc.params(config: LogStruct::Configuration).void).void }
  21. 2 def configure(&block)
  22. 55 yield(config)
  23. end
  24. 4 sig { returns(LogStruct::Configuration) }
  25. 2 def config
  26. 61113 LogStruct::Configuration.instance
  27. end
  28. # (Can't use alias_method since this module is extended into LogStruct)
  29. 3 sig { returns(LogStruct::Configuration) }
  30. 2 def configuration
  31. 905 config
  32. end
  33. # Setter method to replace the configuration (for testing purposes)
  34. 3 sig { params(config: LogStruct::Configuration).void }
  35. 2 def configuration=(config)
  36. 1084 LogStruct::Configuration.set_instance(config)
  37. end
  38. 4 sig { returns(T::Boolean) }
  39. 2 def enabled?
  40. 44 config.enabled
  41. end
  42. 4 sig { void }
  43. 2 def set_enabled_from_rails_env!
  44. # 1. When LOGSTRUCT_ENABLED is set, it decides: on for true, t, yes, y or 1, off for anything else.
  45. # 2. Otherwise LogStruct is on when the Rails environment is in enabled_environments, the process
  46. # isn't a console, and it's a server (server_process?) or runs in the test environment.
  47. 28 env_var_enabled = enabled_from_env_var
  48. 28 config.enabled = if env_var_enabled.nil?
  49. 22 is_console = console_process?
  50. 22 is_server = server_process?
  51. 22 in_enabled_env = config.enabled_environments.include?(::Rails.env.to_sym)
  52. 22 in_enabled_env && !is_console && (is_server || ::Rails.env.test?)
  53. else
  54. 6 env_var_enabled
  55. end
  56. # apply_initializer_settings! compares the settings with these
  57. 28 @enabled_from_rails_env = T.let(config.enabled, T.nilable(T::Boolean))
  58. 28 @enabled_environments_from_rails_env = T.let(config.enabled_environments.dup, T.nilable(T::Array[Symbol]))
  59. end
  60. # After config/initializers run: LOGSTRUCT_ENABLED takes precedence, then `config.enabled`
  61. # if an initializer changed it, then `config.enabled_environments` if an initializer changed that
  62. 4 sig { void }
  63. 2 def apply_initializer_settings!
  64. 5 env_var_enabled = enabled_from_env_var
  65. 5 if !env_var_enabled.nil?
  66. 2 config.enabled = env_var_enabled
  67. 3 elsif config.enabled == @enabled_from_rails_env && config.enabled_environments != @enabled_environments_from_rails_env
  68. 1 set_enabled_from_rails_env!
  69. end
  70. end
  71. 3 sig { returns(T::Boolean) }
  72. 2 def is_local?
  73. 13 config.local_environments.include?(::Rails.env.to_sym)
  74. end
  75. 3 sig { returns(T::Boolean) }
  76. 2 def is_production?
  77. 13 !is_local?
  78. end
  79. 4 sig { void }
  80. 2 def merge_rails_filter_parameters!
  81. 15 return unless ::Rails.application.config.respond_to?(:filter_parameters)
  82. 15 rails_filter_params = ::Rails.application.config.filter_parameters
  83. 15 return unless rails_filter_params.is_a?(Array)
  84. 15 return if rails_filter_params.empty?
  85. 63 blocks, patterns = rails_filter_params.partition { |entry| callable_filter?(entry) }
  86. # Rails matches entries with a dot against a key's path (user.password), and the rest against the key
  87. 59 path_patterns, key_patterns = patterns.partition { |entry| path_pattern?(entry) }
  88. 56 regexps, partial_names = key_patterns.partition { |entry| entry.is_a?(Regexp) }
  89. # Rails gives blocks with any arity but 2 the parameters as a third argument
  90. 19 path_blocks, key_blocks = blocks.partition { |block| arity_for_filter(block) == 3 }
  91. 17 matchers = key_blocks.map { |block| build_callable_filter_matcher(block) }
  92. 15 if partial_names.any?
  93. 8 matchers << build_rails_pattern_matcher(partial_names, label: RAILS_PATTERNS_LABEL, matches_logstruct_fields: false)
  94. end
  95. 15 if regexps.any?
  96. 2 matchers << build_rails_pattern_matcher(regexps, label: RAILS_REGEXPS_LABEL, matches_logstruct_fields: true)
  97. end
  98. 17 path_matchers = path_blocks.map { |block| build_path_block_matcher(block) }
  99. 15 path_matchers << build_rails_path_matcher(path_patterns) if path_patterns.any?
  100. # Exact names also filter LogStruct's own fields, which the partial matches skip
  101. 56 names = key_patterns.filter_map { |entry| entry.to_s.downcase.to_sym if entry.is_a?(Symbol) || entry.is_a?(String) }
  102. 15 config.filters.filter_keys |= names
  103. # The latest list replaces the patterns merged before
  104. 15 registered = config.filters.filter_matchers
  105. 42 registered.reject! { |matcher| RAILS_PATTERN_LABELS.include?(matcher.label) }
  106. 15 matchers.each do |matcher|
  107. 25 registered << matcher unless registered.any? { |existing| existing.label == matcher.label }
  108. end
  109. 15 registered_paths = config.filters.path_matchers
  110. 15 registered_paths.reject! { |matcher| matcher.label == RAILS_PATHS_LABEL }
  111. 15 path_matchers.each do |matcher|
  112. 4 registered_paths << matcher unless registered_paths.any? { |existing| existing.label == matcher.label }
  113. end
  114. end
  115. 2 private
  116. 4 sig { returns(T.nilable(T::Boolean)) }
  117. 2 def enabled_from_env_var
  118. 33 value = ENV["LOGSTRUCT_ENABLED"]
  119. 33 return nil if value.nil?
  120. 8 %w[true t yes y 1].include?(value.strip.downcase)
  121. end
  122. 3 sig { returns(T::Boolean) }
  123. 2 def console_process?
  124. 22 return true if defined?(::Rails::Console)
  125. 81 current_argv.any? { |arg| CONSOLE_COMMAND_ARGS.include?(arg) }
  126. end
  127. 3 sig { returns(T::Boolean) }
  128. 2 def server_process?
  129. 22 return true if logstruct_server_mode?
  130. 22 return true if puma_server?
  131. 20 return true if defined?(::Unicorn::HttpServer)
  132. 20 return true if defined?(::Thin::Server)
  133. 20 return true if defined?(::Falcon::Server)
  134. 20 return true if defined?(::Rails::Server)
  135. 19 return true if sidekiq_server?
  136. 19 return true if good_job_cli?
  137. 18 false
  138. end
  139. 3 sig { returns(T::Boolean) }
  140. 2 def puma_server?
  141. # Just checking defined?(::Puma::Server) is not reliable - Puma might be installed
  142. # but not running. Check $PROGRAM_NAME and ARGV to verify we're actually running puma.
  143. # ARGV check is needed when running through wrapper scripts like gosu.
  144. 22 return true if $PROGRAM_NAME.include?("puma")
  145. 63 return true if current_argv.any? { |arg| arg.include?("puma") }
  146. 20 false
  147. end
  148. 3 sig { returns(T::Boolean) }
  149. 2 def sidekiq_server?
  150. 19 !!(defined?(::Sidekiq) && ::Sidekiq.respond_to?(:server?) && ::Sidekiq.server?)
  151. end
  152. # GoodJob's executable (`good_job start`) sets within_exe before it loads the app
  153. 3 sig { returns(T::Boolean) }
  154. 2 def good_job_cli?
  155. 19 !!(defined?(::GoodJob::CLI) && ::GoodJob::CLI.respond_to?(:within_exe?) && ::GoodJob::CLI.within_exe?)
  156. end
  157. 3 sig { returns(T::Boolean) }
  158. 2 def logstruct_server_mode?
  159. 22 ::LogStruct.server_mode?
  160. end
  161. 3 sig { returns(T::Array[String]) }
  162. 2 def current_argv
  163. 42 raw = ::ARGV
  164. 164 strings = raw.map { |arg| arg.to_s }
  165. 42 T.let(strings, T::Array[String])
  166. rescue NameError
  167. EMPTY_ARGV
  168. end
  169. # Rails matches Symbols and Strings as case-insensitive substrings (:passw filters
  170. # "user_password") and Regexps as they are. Entries with a dot match a key by its path
  171. # instead (build_rails_path_matcher).
  172. #
  173. # The substring matches skip LogStruct's own fields (Rails' :_key would hide GoodJob's
  174. # cron_key); their exact names are added to filter_keys instead. A Regexp says exactly
  175. # what it matches, so Regexps apply to every key, LogStruct's fields included.
  176. 4 sig { params(patterns: T::Array[T.untyped], label: String, matches_logstruct_fields: T::Boolean).returns(ConfigStruct::FilterMatcher) }
  177. 2 def build_rails_pattern_matcher(patterns, label:, matches_logstruct_fields:)
  178. 10 parameter_filter = ::ActiveSupport::ParameterFilter.new(patterns, mask: RAILS_FILTER_MASK)
  179. 10 callable = Kernel.lambda do |key, _value|
  180. 2005 parameter_filter.filter_param(key, RAILS_FILTER_PROBE) == RAILS_FILTER_MASK
  181. end
  182. 10 ConfigStruct::FilterMatcher.new(callable: callable, label: label, matches_logstruct_fields: matches_logstruct_fields)
  183. end
  184. 4 sig { params(filter: T.untyped).returns(T::Boolean) }
  185. 2 def callable_filter?(filter)
  186. 48 filter.respond_to?(:call)
  187. end
  188. # Rails treats Strings and Symbols with a dot, and Regexps with an escaped dot, as paths
  189. 4 sig { params(entry: T.untyped).returns(T::Boolean) }
  190. 2 def path_pattern?(entry)
  191. 44 if entry.is_a?(Regexp)
  192. 3 entry.source.include?("\\.")
  193. else
  194. 41 entry.to_s.include?(".")
  195. end
  196. end
  197. # Rails joins a nested parameter's keys with dots ("user.password") and matches its dotted
  198. # entries against that, as case-insensitive substrings or as Regexps. LogStruct's paths
  199. # start at the line's field ("params.user.password"), so an entry matches the path from
  200. # there or from the field's value down, as it would in Rails.
  201. 3 sig { params(patterns: T::Array[T.untyped]).returns(ConfigStruct::PathFilterMatcher) }
  202. 2 def build_rails_path_matcher(patterns)
  203. 5 regexps = patterns.select { |entry| entry.is_a?(Regexp) }
  204. 7 strings = patterns.reject { |entry| entry.is_a?(Regexp) }.map { |entry| Regexp.escape(entry.to_s) }
  205. 2 regexps << Regexp.new(strings.join("|"), true) if strings.any?
  206. 2 callable = Kernel.lambda do |path, _value, _root|
  207. 23 full_path = path.join(".")
  208. 23 field_path = path.drop(1).join(".")
  209. 53 regexps.any? { |regexp| regexp.match?(full_path) || (!field_path.empty? && regexp.match?(field_path)) }
  210. end
  211. 2 ConfigStruct::PathFilterMatcher.new(callable: callable, label: RAILS_PATHS_LABEL, uses_root: false)
  212. end
  213. # A Rails filter block with three arguments, given the parameters around the key: the
  214. # line's top-level field's value (a request line's params), or the line for a top-level key
  215. 3 sig { params(filter: T.untyped).returns(ConfigStruct::PathFilterMatcher) }
  216. 2 def build_path_block_matcher(filter)
  217. 2 callable = Kernel.lambda do |path, original_value, root|
  218. 10 original_key = path.last.to_s
  219. 10 key = original_key.dup
  220. 10 value = original_value.duplicable? ? original_value.dup : original_value
  221. 10 result = filter.call(key, value, root)
  222. 10 !!result || (value.is_a?(String) && value != original_value)
  223. rescue => e
  224. handle_filter_error(e, filter, original_key || "")
  225. false
  226. end
  227. 2 ConfigStruct::PathFilterMatcher.new(callable: callable, label: filter.inspect)
  228. end
  229. # Rails gives filter blocks copies of the key and value, ignores what they return, and
  230. # logs the value as the block left it: the Rails guides' example reverses it in place,
  231. # and a block can replace it with "[FILTERED]". LogStruct filters a key when its block
  232. # returns a truthy value or changes a String it's given, and never changes the logged
  233. # value itself. Blocks with three arguments are path matchers (build_path_block_matcher).
  234. 3 sig { params(filter: T.untyped).returns(ConfigStruct::FilterMatcher) }
  235. 2 def build_callable_filter_matcher(filter)
  236. 2 callable = Kernel.lambda do |original_key, original_value|
  237. 5 key = original_key.dup
  238. 5 value = original_value.duplicable? ? original_value.dup : original_value
  239. 5 call_args = case arity_for_filter(filter)
  240. when 0
  241. []
  242. when 1
  243. [key]
  244. else
  245. 5 [key, value]
  246. end
  247. 5 result = filter.call(*call_args)
  248. 5 !!result || (value.is_a?(String) && value != original_value)
  249. rescue ArgumentError
  250. begin
  251. !!filter.call(key)
  252. rescue => e
  253. handle_filter_error(e, filter, key)
  254. false
  255. end
  256. rescue => e
  257. handle_filter_error(e, filter, key)
  258. false
  259. end
  260. 2 ConfigStruct::FilterMatcher.new(callable: callable, label: filter.inspect)
  261. end
  262. 3 sig { params(filter: T.untyped).returns(Integer) }
  263. 2 def arity_for_filter(filter)
  264. 9 filter.respond_to?(:arity) ? filter.arity : 2
  265. end
  266. 2 sig { params(error: StandardError, filter: T.untyped, key: String).void }
  267. 2 def handle_filter_error(error, filter, key)
  268. context = {
  269. filter: filter.class.name,
  270. key: key,
  271. filter_label: begin
  272. filter.inspect
  273. rescue
  274. "unknown"
  275. end
  276. }
  277. LogStruct.handle_exception(error, source: Source::Internal, context: context)
  278. end
  279. end
  280. end
  281. end
  282. end

lib/log_struct/concerns/error_handling.rb

91.67% lines covered

48 relevant lines. 44 lines covered and 4 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Concerns
  5. # Concern for handling errors according to configured modes
  6. 2 module ErrorHandling
  7. 2 module ClassMethods
  8. 2 extend T::Sig
  9. 2 extend T::Helpers
  10. # Needed for raise
  11. 2 requires_ancestor { Module }
  12. # Get the error handling mode for a given source
  13. 3 sig { params(source: Source).returns(ErrorHandlingMode) }
  14. 2 def error_handling_mode_for(source)
  15. 59 config = LogStruct.config
  16. # Use a case statement for type-safety
  17. 59 case source
  18. when Source::TypeChecking
  19. 9 config.error_handling_modes.type_checking_errors
  20. when Source::Internal
  21. 8 config.error_handling_modes.logstruct_errors
  22. when Source::Security
  23. 2 config.error_handling_modes.security_errors
  24. when Source::Rails, Source::App, Source::Job, Source::Storage, Source::Mailer,
  25. Source::Shrine, Source::CarrierWave, Source::Sidekiq, Source::Dotenv, Source::Puma
  26. 40 config.error_handling_modes.standard_errors
  27. else
  28. # Ensures the case statement is exhaustive
  29. T.absurd(source)
  30. end
  31. end
  32. # Log an errors with structured data
  33. 3 sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
  34. 2 def log_error(error, source:, context: nil)
  35. # Create structured log entry
  36. 17 error_log = Log.from_exception(source, error, context || {})
  37. 17 LogStruct.error(error_log)
  38. end
  39. # Log an error and report it using the configured handler or MultiErrorReporter
  40. 2 sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
  41. 2 def log_and_report_error(error, source:, context: nil)
  42. log_error(error, source: source, context: context)
  43. report_error(error, source: source, context: context)
  44. end
  45. # Report an error using the configured handler or MultiErrorReporter, without logging it. A reporter that
  46. # raises (a timeout calling its service) doesn't change what happens to the error: its own error is logged,
  47. # and not reported through the same reporter.
  48. 3 sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
  49. 2 def report_error(error, source:, context: nil)
  50. 20 context = filtered_context(context)
  51. 20 error_handler = LogStruct.config.error_reporting_handler
  52. 20 if error_handler
  53. # Use the configured handler
  54. 15 error_handler.call(error, context, source)
  55. else
  56. # Fall back to MultiErrorReporter (detects Sentry, Bugsnag, etc.)
  57. 5 LogStruct::MultiErrorReporter.report_error(error, context || {})
  58. end
  59. rescue => e
  60. 4 LogStruct.error(Log::Error.new(
  61. source: Source::Internal,
  62. error_class: e.class,
  63. message: "The error reporter raised while reporting #{error.class}",
  64. error_message: e.message,
  65. backtrace: e.backtrace,
  66. level: Level::Error
  67. ))
  68. end
  69. # The context filtered and scrubbed like a log line, for error reporters: it can hold a
  70. # request's parameters or a mail's recipients
  71. 3 sig { params(context: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
  72. 2 def filtered_context(context)
  73. 20 return context if context.nil? || context.empty?
  74. 13 filtered = Formatter.new.process_values(context)
  75. 13 filtered.is_a?(Hash) ? filtered : {}
  76. end
  77. # Handle an error according to the configured error handling mode (log, report, raise, etc).
  78. # Pass `log: false` when the caller has already logged the error in its own structured line.
  79. 3 sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped]), log: T::Boolean).void }
  80. 2 def handle_exception(error, source:, context: nil, log: true)
  81. 34 mode = error_handling_mode_for(source)
  82. # Log / report in production, raise locally (dev/test)
  83. 34 if mode == ErrorHandlingMode::LogProduction || mode == ErrorHandlingMode::ReportProduction
  84. 18 raise(error) if !LogStruct.is_production?
  85. end
  86. 24 case mode
  87. when ErrorHandlingMode::Ignore
  88. # Do nothing
  89. when ErrorHandlingMode::Raise
  90. 2 raise(error)
  91. when ErrorHandlingMode::Log, ErrorHandlingMode::LogProduction
  92. 11 log_error(error, source: source, context: context) if log
  93. when ErrorHandlingMode::Report, ErrorHandlingMode::ReportProduction
  94. 10 log_error(error, source: source, context: context) if log
  95. 10 report_error(error, source: source, context: context)
  96. else
  97. # Ensures the case statement is exhaustive
  98. T.absurd(mode)
  99. end
  100. end
  101. end
  102. end
  103. end
  104. end

lib/log_struct/concerns/logging.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../log"
  4. 2 module LogStruct
  5. 2 module Concerns
  6. # Concern for handling errors according to configured modes
  7. 2 module Logging
  8. 2 module ClassMethods
  9. 2 extend T::Sig
  10. # Log a log struct at debug level
  11. 3 sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
  12. 2 def debug(log)
  13. 1 Rails.logger.debug(log)
  14. end
  15. # Log a log struct at info level
  16. 4 sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
  17. 2 def info(log)
  18. 66 Rails.logger.info(log)
  19. end
  20. # Log a log struct at warn level
  21. 3 sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
  22. 2 def warn(log)
  23. 1 Rails.logger.warn(log)
  24. end
  25. # Log a log struct at error level
  26. 3 sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
  27. 2 def error(log)
  28. 68 Rails.logger.error(log)
  29. end
  30. # Log a log struct at fatal level
  31. 3 sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
  32. 2 def fatal(log)
  33. 1 Rails.logger.fatal(log)
  34. end
  35. end
  36. end
  37. end
  38. end

lib/log_struct/config_struct/error_handling_modes.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module ConfigStruct
  5. 2 class ErrorHandlingModes < T::Struct
  6. 2 include Sorbet::SerializeSymbolKeys
  7. # How to handle different types of errors
  8. # Modes:
  9. # - Ignore - Ignore the error
  10. # - Log - Log the error
  11. # - Report - Log and report to error tracking service (but don't crash)
  12. # - LogProduction - Log error in production, raise locally (dev/test)
  13. # - ReportProduction - Report error in production, raise locally (dev/test)
  14. # - Raise - Always raise the error
  15. # Configurable error handling categories
  16. # Sorbet type errors in your app and its gems: logged and reported in production, so they don't pass unseen
  17. 2 prop :type_checking_errors, ErrorHandlingMode, default: ErrorHandlingMode::ReportProduction
  18. # LogStruct's own errors
  19. 2 prop :logstruct_errors, ErrorHandlingMode, default: ErrorHandlingMode::LogProduction
  20. 2 prop :security_errors, ErrorHandlingMode, default: ErrorHandlingMode::Report
  21. 2 prop :standard_errors, ErrorHandlingMode, default: ErrorHandlingMode::Raise
  22. end
  23. end
  24. end

lib/log_struct/config_struct/filters.rb

98.36% lines covered

61 relevant lines. 60 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/hash_with_indifferent_access"
  4. 2 module LogStruct
  5. # A key's words, lowercase and joined by underscores, so that keys match however they're written:
  6. # apiKey, api-key, X-Api-Key and HTTP_X_API_KEY all contain "api_key"
  7. 2 module KeyWords
  8. 2 extend T::Sig
  9. 4 sig { params(key: T.untyped).returns(String) }
  10. 2 def self.normalize(key)
  11. 24652 key.to_s
  12. .gsub(/([a-z\d])([A-Z])/, '\1_\2')
  13. .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
  14. .tr("- .", "___")
  15. .downcase
  16. end
  17. end
  18. 2 module ConfigStruct
  19. 2 class FilterMatcher < T::Struct
  20. 2 extend T::Sig
  21. 2 const :callable, T.proc.params(key: String, value: T.untyped).returns(T::Boolean)
  22. 2 const :label, String
  23. # Whether the matcher also applies to the names of LogStruct's own fields at the top of a line.
  24. # LogStruct's matcher for Rails' partial matches doesn't: Rails' default :_key would filter cron_key.
  25. 2 const :matches_logstruct_fields, T::Boolean, default: true
  26. 4 sig { params(key: String, value: T.untyped).returns(T::Boolean) }
  27. 2 def matches?(key, value)
  28. 14273 callable.call(key, value)
  29. end
  30. end
  31. # A filter that needs more than the key: Rails' entries with a dot (`card.number`) match a key by its
  32. # path, and Rails gives filter blocks with three arguments the parameters around the key. The path
  33. # starts at the line's top-level field (`["params", "card", "number"]`); `root` is that field's value,
  34. # or the whole line for a top-level key, with indifferent access as Rails gives it. Building it copies
  35. # every value inside it, so a matcher that doesn't read it sets uses_root to false and gets nil.
  36. 2 class PathFilterMatcher < T::Struct
  37. 2 extend T::Sig
  38. 2 const :callable, T.proc.params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean)
  39. 2 const :label, String
  40. 2 const :uses_root, T::Boolean, default: true
  41. 3 sig { params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean) }
  42. 2 def matches?(path, value, root)
  43. 35 root = nil unless uses_root
  44. 35 callable.call(path, value, root.is_a?(LazyRoot) ? root.value : root)
  45. end
  46. end
  47. # The root a PathFilterMatcher gets, built the first time a matcher reads it
  48. 2 class LazyRoot
  49. 2 extend T::Sig
  50. 3 sig { params(value: T.untyped).void }
  51. 2 def initialize(value)
  52. 16 @source = value
  53. 16 @built = T.let(false, T::Boolean)
  54. 16 @value = T.let(nil, T.untyped)
  55. end
  56. # The value with indifferent access, as Rails gives filter blocks the request's parameters
  57. 3 sig { returns(T.untyped) }
  58. 2 def value
  59. 8 return @value if @built
  60. 4 @built = true
  61. @value = begin
  62. 4 @source.is_a?(Hash) ? ActiveSupport::HashWithIndifferentAccess.new(@source) : @source
  63. rescue SystemStackError, StandardError
  64. @source
  65. end
  66. end
  67. end
  68. 2 class Filters < T::Struct
  69. 2 include Sorbet::SerializeSymbolKeys
  70. # Keys that should be filtered in nested structures such as request params and job arguments.
  71. # A filtered value is replaced by a summary of it, with no length (which would say how long a
  72. # secret is):
  73. #
  74. # { _filtered: { _class: "Hash", _keys_count: 3, _keys: [:key1, :key2, :key3] } } # first 10 keys
  75. # { _filtered: { _class: "Array", _count: 2 } }
  76. # { _filtered: { _class: "String" } } # plus its _hash for filter_keys_with_hashes
  77. #
  78. # A key matches when its name, or its words joined by underscores (KeyWords.normalize), equal an
  79. # entry: :api_key also filters apiKey and X-Api-Key, but doesn't by itself filter api_key_id (the
  80. # default filter_matchers' sensitive words do).
  81. #
  82. # Default: [:password, :password_confirmation, :pass, :pw, :token, :secret, :credentials, :auth,
  83. # :authentication, :authorization, :credit_card, :ssn, :social_security]
  84. #
  85. 2 prop :filter_keys,
  86. T::Array[Symbol],
  87. factory: -> {
  88. 113 %i[
  89. password password_confirmation pass pw token secret
  90. credentials auth authentication authorization
  91. credit_card ssn social_security
  92. ]
  93. }
  94. # Keys that are filtered like filter_keys, and whose String values also get a _hash in their summary
  95. # (see hash_salt), so lines about the same person can be followed without logging the address.
  96. # Default: [:email, :email_address]
  97. 2 prop :filter_keys_with_hashes,
  98. T::Array[Symbol],
  99. 113 factory: -> { %i[email email_address] }
  100. # The key for the HMAC-SHA256 hashes of emails and filter_keys_with_hashes values. Anyone who has
  101. # the key and a list of candidate emails can match them to the hashes, so keep it secret.
  102. # Default: nil, which uses a key derived from the Rails app's secret_key_base. Set it to keep the
  103. # same hashes in every app that logs the same people.
  104. 2 prop :hash_salt, T.nilable(String), default: nil
  105. # How many hex characters of each hash to log
  106. # Default: 16
  107. 2 prop :hash_length, Integer, default: 16
  108. # Filter email addresses. Also controls email filtering for the ActionMailer integration
  109. # (to, from, recipient fields, etc.)
  110. # Default: true
  111. 2 prop :email_addresses, T::Boolean, default: true
  112. # Filter URL passwords
  113. # Default: true
  114. 2 prop :url_passwords, T::Boolean, default: true
  115. # Filter sensitive params in URLs' query strings: params that filter_keys or the filter matchers
  116. # would filter as keys (?token=, ?api_key=, user[password]=), and OAuth's ?code=
  117. # Default: true
  118. 2 prop :url_query_params, T::Boolean, default: true
  119. # Scrub the credentials after Bearer and Basic in strings (an Authorization header's value,
  120. # logged as text): Bearer tokens of 16 or more characters, and Basic credentials that decode to
  121. # user:password
  122. # Default: true
  123. 2 prop :authorization_credentials, T::Boolean, default: true
  124. # Filter credit card numbers
  125. # Default: true
  126. 2 prop :credit_card_numbers, T::Boolean, default: true
  127. # Filter phone numbers
  128. # Default: true
  129. 2 prop :phone_numbers, T::Boolean, default: true
  130. # Filter social security numbers
  131. # Default: true
  132. 2 prop :ssns, T::Boolean, default: true
  133. # Filter IP addresses
  134. # Default: false
  135. 2 prop :ip_addresses, T::Boolean, default: false
  136. # Filter MAC addresses
  137. # Default: false
  138. 2 prop :mac_addresses, T::Boolean, default: false
  139. # Default pattern for sensitive keys, matched against a key's words joined by underscores
  140. # (KeyWords.normalize: apiKey, api-key and API_KEY are all "api_key"):
  141. # - these words anywhere in the key: password, passwd, passphrase, passcode, pwd, token, secret, auth,
  142. # authorization, authentication, cred, credential, cookie, session, jwt, bearer, otp, apikey, cvv,
  143. # cvc, ssn, social_security, social_security_number, credit_card, card_number, crypt, crypted (and
  144. # their plurals): access_token, newPassword, Set-Cookie, HTTP_AUTHORIZATION, credit_card_number
  145. # - api_key, secret_key, private_key, access_key, encryption_key, signing_key, master_key: X-Api-Key
  146. # - signature, sig, salt and certificate as the last word: X-Amz-Signature (but not signature_field_id)
  147. # Matching whole words avoids false positives like "keyboard", "author" or "cron_key".
  148. 2 DEFAULT_SENSITIVE_KEY_PATTERN = T.let(
  149. /(?:\A|_)(?:passwords?|passwd|passphrases?|passcodes?|pwd|tokens?|secrets?|auth|authorization|authentication|creds?|credentials?|cookies?|sessions?|jwts?|bearer|otps?|apikeys?|cvv|cvc|ssn|social_security(?:_numbers?)?|credit_cards?|card_numbers?|crypt|crypted)(?:_|\z)|(?:\A|_)(?:api|secret|private|access|encryption|signing|master)_keys?(?:_|\z)|(?:\A|_)(?:signature|sig|salt|certificate)s?\z/,
  150. Regexp
  151. )
  152. # Matchers for keys that aren't listed by name in filter_keys. Each matcher receives the key (String)
  153. # and optional value, returning true when the pair should be filtered. Includes a regex matcher for
  154. # common sensitive key patterns, and LogStruct adds matchers for Rails' filter_parameters at boot.
  155. 2 prop :filter_matchers,
  156. T::Array[FilterMatcher],
  157. factory: -> {
  158. [
  159. 113 FilterMatcher.new(
  160. 12258 callable: ->(key, _value) { DEFAULT_SENSITIVE_KEY_PATTERN.match?(KeyWords.normalize(key)) },
  161. label: "default_sensitive_pattern"
  162. )
  163. ]
  164. }
  165. # Matchers that need a key's path or the parameters around it (see PathFilterMatcher). LogStruct
  166. # adds them for Rails' filter_parameters at boot.
  167. 2 prop :path_matchers,
  168. T::Array[PathFilterMatcher],
  169. 113 factory: -> { [] }
  170. end
  171. end
  172. end

lib/log_struct/config_struct/integrations.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/notifications"
  4. 2 module LogStruct
  5. 2 module ConfigStruct
  6. 2 class Integrations < T::Struct
  7. 2 include Sorbet::SerializeSymbolKeys
  8. # Enable or disable Sorbet error handler integration
  9. # Default: true
  10. 2 prop :enable_sorbet_error_handlers, T::Boolean, default: true
  11. # Enable or disable Lograge integration
  12. # Default: true
  13. 2 prop :enable_lograge, T::Boolean, default: true
  14. # Custom options for Lograge
  15. # Default: nil
  16. 2 prop :lograge_custom_options, T.nilable(Handlers::LogrageCustomOptions), default: nil
  17. # Enable or disable ActionMailer integration
  18. # Default: true
  19. 2 prop :enable_actionmailer, T::Boolean, default: true
  20. # Map instance variables on mailer to ID fields in additional_data
  21. # Default: { account: :account_id, user: :user_id }
  22. # Example: { organization: :org_id, company: :company_id }
  23. 101 prop :actionmailer_id_mapping, T::Hash[Symbol, Symbol], factory: -> { {account: :account_id, user: :user_id} }
  24. # Enable or disable host authorization logging
  25. # Default: true
  26. 2 prop :enable_host_authorization, T::Boolean, default: true
  27. # Enable or disable ActiveJob integration
  28. # Default: true
  29. 2 prop :enable_activejob, T::Boolean, default: true
  30. # Enable or disable Rack middleware
  31. # Default: true
  32. 2 prop :enable_rack_error_handler, T::Boolean, default: true
  33. # Enable or disable Sidekiq integration
  34. # Default: true
  35. 2 prop :enable_sidekiq, T::Boolean, default: true
  36. # Enable or disable Shrine integration
  37. # Default: true
  38. 2 prop :enable_shrine, T::Boolean, default: true
  39. # Enable or disable ActiveStorage integration
  40. # Default: true
  41. 2 prop :enable_activestorage, T::Boolean, default: true
  42. # Enable or disable CarrierWave integration
  43. # Default: true
  44. 2 prop :enable_carrierwave, T::Boolean, default: true
  45. # Enable or disable GoodJob integration
  46. # Default: true
  47. 2 prop :enable_goodjob, T::Boolean, default: true
  48. # Enable SQL query logging through ActiveRecord instrumentation
  49. # Default: false (can be resource intensive)
  50. 2 prop :enable_sql_logging, T::Boolean, default: false
  51. # Only log SQL queries slower than this threshold (in milliseconds)
  52. # Set to 0 or nil to log all queries
  53. # Default: 100.0 (log queries taking >100ms)
  54. 2 prop :sql_slow_query_threshold, T.nilable(Float), default: 100.0
  55. # Include bind parameters in SQL logs (disable in production for security)
  56. # Default: true in development/test, false in production
  57. 101 prop :sql_log_bind_params, T::Boolean, factory: -> { !defined?(::Rails) || !::Rails.respond_to?(:env) || !::Rails.env.production? }
  58. # Enable Ahoy (analytics events) integration
  59. # Default: true (safe no-op unless Ahoy is defined)
  60. 2 prop :enable_ahoy, T::Boolean, default: true
  61. # Enable ActiveModelSerializers integration
  62. # Default: true (safe no-op unless ActiveModelSerializers is defined)
  63. 2 prop :enable_active_model_serializers, T::Boolean, default: true
  64. # Enable dotenv-rails integration (convert to structured logs)
  65. # Default: true
  66. 2 prop :enable_dotenv, T::Boolean, default: true
  67. # Enable Puma integration (convert server lifecycle logs)
  68. # Default: true
  69. 2 prop :enable_puma, T::Boolean, default: true
  70. end
  71. end
  72. end

lib/log_struct/config_struct/limits.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module ConfigStruct
  5. # How much of each logged value LogStruct writes. The limits keep a huge value (a request body,
  6. # a large Hash) from slowing down the thread that writes the logs.
  7. 2 class Limits < T::Struct
  8. 2 include Sorbet::SerializeSymbolKeys
  9. # The most keys written from a Hash. The rest are counted in _more_keys.
  10. # Default: 100
  11. 2 prop :max_hash_keys, Integer, default: 100
  12. # The most items written from an Array, followed by "... and N more items"
  13. # Default: 10
  14. 2 prop :max_array_items, Integer, default: 10
  15. # The most lines written from an Array that looks like a backtrace, such as an error line's
  16. # backtrace. The rest are counted in a last "... and N more lines" item.
  17. # Default: 20
  18. 2 prop :max_backtrace_lines, Integer, default: 20
  19. # Strings longer than this many bytes are cut, and say how long they were
  20. # Default: 32768 (32 KB)
  21. 2 prop :max_string_bytes, Integer, default: 32_768
  22. end
  23. end
  24. end

lib/log_struct/configuration.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "handlers"
  4. 2 require_relative "config_struct/error_handling_modes"
  5. 2 require_relative "config_struct/integrations"
  6. 2 require_relative "config_struct/filters"
  7. 2 require_relative "config_struct/limits"
  8. 2 module LogStruct
  9. # Core configuration class that provides a type-safe API
  10. 2 class Configuration < T::Struct
  11. 2 extend T::Sig
  12. 2 include Sorbet::SerializeSymbolKeys
  13. # -------------------------------------------------------------------------------------
  14. # Props
  15. # -------------------------------------------------------------------------------------
  16. 2 prop :enabled, T::Boolean, default: true
  17. 100 prop :enabled_environments, T::Array[Symbol], factory: -> { [:test, :production] }
  18. 100 prop :local_environments, T::Array[Symbol], factory: -> { [:development, :test] }
  19. # Prefer production-style JSON in development when LogStruct is enabled
  20. 2 prop :prefer_json_in_development, T::Boolean, default: true
  21. # Enable colorful human formatter in development
  22. 2 prop :enable_color_output, T::Boolean, default: true
  23. # Custom color map for the color formatter
  24. 2 prop :color_map, T.nilable(T::Hash[Symbol, Symbol]), default: nil
  25. # Filter noisy loggers (ActionView, etc.)
  26. 2 prop :filter_noisy_loggers, T::Boolean, default: false
  27. 100 const :integrations, ConfigStruct::Integrations, factory: -> { ConfigStruct::Integrations.new }
  28. 71 const :filters, ConfigStruct::Filters, factory: -> { ConfigStruct::Filters.new }
  29. 100 const :limits, ConfigStruct::Limits, factory: -> { ConfigStruct::Limits.new }
  30. # Custom log scrubbing handler for any additional string scrubbing
  31. # Default: nil
  32. 2 prop :string_scrubbing_handler, T.nilable(Handlers::StringScrubber)
  33. # Custom handler for error reporting
  34. # Default: Errors are handled by MultiErrorReporter
  35. # (auto-detects Sentry, Bugsnag, Rollbar, Honeybadger, etc.)
  36. 2 prop :error_reporting_handler, T.nilable(Handlers::ErrorReporter), default: nil
  37. # How to handle errors from various sources
  38. 2 const :error_handling_modes,
  39. ConfigStruct::ErrorHandlingModes,
  40. factory: -> {
  41. 98 ConfigStruct::ErrorHandlingModes.new
  42. }
  43. # -------------------------------------------------------------------------------------
  44. # Class Methods
  45. # -------------------------------------------------------------------------------------
  46. # Class‐instance variable
  47. 2 @instance = T.let(nil, T.nilable(Configuration))
  48. 4 sig { returns(Configuration) }
  49. 2 def self.instance
  50. 61140 @instance ||= T.let(Configuration.new, T.nilable(Configuration))
  51. end
  52. 3 sig { params(config: Configuration).void }
  53. 2 def self.set_instance(config)
  54. 1084 @instance = config
  55. end
  56. end
  57. end

lib/log_struct/enums.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # Require all enums in this directory
  4. 2 require_relative "enums/error_handling_mode"
  5. 2 require_relative "enums/error_reporter"
  6. 2 require_relative "enums/event"
  7. 2 require_relative "enums/level"
  8. 2 require_relative "enums/source"

lib/log_struct/enums/error_handling_mode.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Enum for error handling modes
  5. 2 class ErrorHandlingMode < T::Enum
  6. 2 enums do
  7. # Always ignore the error
  8. 2 Ignore = new(:ignore)
  9. # Always log the error
  10. 2 Log = new(:log)
  11. # Always report to tracking service and continue
  12. 2 Report = new(:report)
  13. # Log in production, raise locally (dev/test)
  14. 2 LogProduction = new(:log_production)
  15. # Report in production, raise locally (dev/test)
  16. 2 ReportProduction = new(:report_production)
  17. # Always raise regardless of environment
  18. 2 Raise = new(:raise)
  19. end
  20. end
  21. end

lib/log_struct/enums/error_reporter.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 class ErrorReporter < T::Enum
  5. 2 enums do
  6. 2 RailsLogger = new(:rails_logger)
  7. 2 Sentry = new(:sentry)
  8. 2 Bugsnag = new(:bugsnag)
  9. 2 Rollbar = new(:rollbar)
  10. 2 Honeybadger = new(:honeybadger)
  11. end
  12. end
  13. end

lib/log_struct/enums/event.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Define log event types as an enum
  5. 2 class Event < T::Enum
  6. 2 enums do
  7. # Plain log messages
  8. 2 Log = new(:log)
  9. # Request events
  10. 2 Request = new(:request)
  11. # Job events
  12. 2 Enqueue = new(:enqueue)
  13. 2 Schedule = new(:schedule)
  14. 2 Start = new(:start)
  15. 2 Finish = new(:finish)
  16. 2 Retry = new(:retry)
  17. 2 RetryStopped = new(:retry_stopped)
  18. 2 Discard = new(:discard)
  19. # File storage events (ActiveStorage, Shrine, CarrierWave, etc.)
  20. 2 Upload = new(:upload)
  21. 2 Download = new(:download)
  22. 2 Delete = new(:delete)
  23. 2 Metadata = new(:metadata)
  24. 2 Exist = new(:exist)
  25. 2 Url = new(:url)
  26. # Data generation events
  27. 2 Generate = new(:generate)
  28. # Email events
  29. 2 Delivery = new(:delivery)
  30. 2 Delivered = new(:delivered)
  31. # Configuration / boot events
  32. 2 Load = new(:load)
  33. 2 Update = new(:update)
  34. 2 Save = new(:save)
  35. 2 Restore = new(:restore)
  36. # Server lifecycle (e.g., Puma)
  37. # Start already defined above
  38. 2 Shutdown = new(:shutdown)
  39. # Security events
  40. 2 IPSpoof = new(:ip_spoof)
  41. 2 CSRFViolation = new(:csrf_violation)
  42. 2 BlockedHost = new(:blocked_host)
  43. # Database events
  44. 2 Database = new(:database)
  45. # Error events
  46. 2 Error = new(:error)
  47. # Fallback
  48. 2 Unknown = new(:unknown)
  49. end
  50. end
  51. end

lib/log_struct/enums/level.rb

100.0% lines covered

43 relevant lines. 43 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "logger"
  4. 2 module LogStruct
  5. # Define log levels as an enum
  6. 2 class Level < T::Enum
  7. 2 extend T::Sig
  8. 2 enums do
  9. # Standard log levels
  10. 2 Debug = new(:debug)
  11. 2 Info = new(:info)
  12. 2 Warn = new(:warn)
  13. 2 Error = new(:error)
  14. 2 Fatal = new(:fatal)
  15. 2 Unknown = new(:unknown)
  16. end
  17. # Convert a Level to the corresponding Logger integer constant
  18. 3 sig { returns(Integer) }
  19. 2 def to_severity_int
  20. 6 case serialize
  21. 1 when :debug then ::Logger::DEBUG
  22. 1 when :info then ::Logger::INFO
  23. 1 when :warn then ::Logger::WARN
  24. 1 when :error then ::Logger::ERROR
  25. 1 when :fatal then ::Logger::FATAL
  26. 1 else ::Logger::UNKNOWN
  27. end
  28. end
  29. # Convert a string or integer severity to a Level
  30. 4 sig { params(severity: T.any(String, Symbol, Integer, NilClass)).returns(Level) }
  31. 2 def self.from_severity(severity)
  32. 1779 return Unknown if severity.nil?
  33. 1778 return from_severity_int(severity) if severity.is_a?(Integer)
  34. 1735 from_severity_sym(severity.downcase.to_sym)
  35. end
  36. 4 sig { params(severity: Symbol).returns(Level) }
  37. 2 def self.from_severity_sym(severity)
  38. 1735 case severity.to_s.downcase.to_sym
  39. 25 when :debug then Debug
  40. 1559 when :info then Info
  41. 23 when :warn then Warn
  42. 119 when :error then Error
  43. 6 when :fatal then Fatal
  44. 3 else Unknown
  45. end
  46. end
  47. 3 sig { params(severity: Integer).returns(Level) }
  48. 2 def self.from_severity_int(severity)
  49. 43 case severity
  50. 1 when ::Logger::DEBUG then Debug
  51. 35 when ::Logger::INFO then Info
  52. 1 when ::Logger::WARN then Warn
  53. 3 when ::Logger::ERROR then Error
  54. 1 when ::Logger::FATAL then Fatal
  55. 2 else Unknown
  56. end
  57. end
  58. end
  59. end

lib/log_struct/enums/log_field.rb

100.0% lines covered

117 relevant lines. 117 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # NOTE:
  4. # - This enum defines human‑readable field names (constants) that map to compact
  5. # JSON key symbols via `serialize` (e.g., Database => :db).
  6. # - The enum constant names are code‑generated into
  7. # `schemas/meta/log-fields.json` by `scripts/generate_structs.rb` and
  8. # referenced from `schemas/meta/log-source-schema.json` to strictly validate
  9. # field keys in `schemas/log_sources/*`.
  10. # - When adding or renaming fields here, run the generator so schema validation
  11. # stays in sync.
  12. #
  13. # Use human-readable field names as the enum values and short field names for the JSON properties
  14. 2 module LogStruct
  15. 2 class LogField < T::Enum
  16. 2 enums do
  17. # Shared fields
  18. 2 Source = new(:src)
  19. 2 Event = new(:evt)
  20. 2 Timestamp = new(:ts)
  21. 2 Level = new(:lvl)
  22. # Common fields
  23. 2 Message = new(:msg)
  24. 2 Data = new(:data)
  25. # Request-related fields
  26. 2 Path = new(:path)
  27. 2 HttpMethod = new(:method) # property name was http_method
  28. 2 SourceIp = new(:source_ip)
  29. 2 UserAgent = new(:user_agent)
  30. 2 Referer = new(:referer)
  31. 2 RequestId = new(:req_id)
  32. 2 Host = new(:host)
  33. 2 ContentType = new(:content_type)
  34. 2 Accept = new(:accept)
  35. # HTTP-specific fields
  36. 2 Format = new(:format)
  37. 2 Controller = new(:controller)
  38. 2 Action = new(:action)
  39. 2 Status = new(:status)
  40. # DurationMs already defined below for general metrics
  41. 2 View = new(:view)
  42. 2 Database = new(:db)
  43. 2 Params = new(:params)
  44. # Security-specific fields
  45. 2 BlockedHost = new(:blocked_host)
  46. 2 BlockedHosts = new(:blocked_hosts)
  47. 2 AllowedHosts = new(:allowed_hosts)
  48. 2 AllowIpHosts = new(:allow_ip_hosts)
  49. 2 ClientIp = new(:client_ip)
  50. 2 XForwardedFor = new(:x_forwarded_for)
  51. # Email-specific fields
  52. 2 To = new(:to)
  53. 2 From = new(:from)
  54. 2 Subject = new(:subject)
  55. 2 MessageId = new(:msg_id)
  56. 2 MailerClass = new(:mailer)
  57. 2 MailerAction = new(:mailer_action)
  58. 2 AttachmentCount = new(:attachments)
  59. # Error fields
  60. 2 ErrorClass = new(:error_class)
  61. 2 Backtrace = new(:backtrace)
  62. # Job-specific fields
  63. 2 JobId = new(:job_id)
  64. 2 JobClass = new(:job_class)
  65. 2 QueueName = new(:queue_name)
  66. 2 Arguments = new(:arguments)
  67. 2 RetryCount = new(:retry_count)
  68. 2 Retries = new(:retries)
  69. 2 Attempt = new(:attempt)
  70. 2 Executions = new(:executions)
  71. 2 ExceptionExecutions = new(:exception_executions)
  72. 2 ProviderJobId = new(:provider_job_id)
  73. 2 ScheduledAt = new(:scheduled_at)
  74. 2 StartedAt = new(:started_at)
  75. 2 FinishedAt = new(:finished_at)
  76. 2 DurationMs = new(:duration_ms)
  77. 2 WaitMs = new(:wait_ms)
  78. # Deprecated: ExecutionTime/WaitTime/RunTime
  79. 2 ExecutionTime = new(:execution_time)
  80. 2 WaitTime = new(:wait_time)
  81. 2 RunTime = new(:run_time)
  82. 2 Priority = new(:priority)
  83. 2 CronKey = new(:cron_key)
  84. 2 ErrorMessage = new(:error_message)
  85. # What the integration was doing when the error happened, e.g. Sidekiq's "Job raised exception"
  86. 2 ErrorContext = new(:error_context)
  87. # The cause of an error the app didn't handle in a request (Exception#cause)
  88. 2 CauseClass = new(:cause_class)
  89. 2 CauseMessage = new(:cause_message)
  90. 2 Result = new(:result)
  91. 2 EnqueueCaller = new(:enqueue_caller)
  92. # Dotenv fields
  93. 2 File = new(:file)
  94. 2 Vars = new(:vars)
  95. 2 Snapshot = new(:snapshot)
  96. # Sidekiq-specific fields
  97. 2 ProcessId = new(:pid)
  98. 2 ThreadId = new(:tid)
  99. 2 Context = new(:ctx)
  100. # Storage-specific fields (ActiveStorage)
  101. 2 Checksum = new(:checksum)
  102. 2 Exist = new(:exist)
  103. 2 Url = new(:url)
  104. 2 Prefix = new(:prefix)
  105. 2 Range = new(:range)
  106. # Storage-specific fields (Shrine)
  107. 2 Storage = new(:storage)
  108. 2 Operation = new(:op)
  109. 2 FileId = new(:file_id)
  110. 2 Filename = new(:filename)
  111. 2 MimeType = new(:mime_type)
  112. 2 Size = new(:size)
  113. 2 Metadata = new(:metadata)
  114. 2 Location = new(:location)
  115. 2 UploadOptions = new(:upload_opts)
  116. 2 DownloadOptions = new(:download_opts)
  117. 2 Options = new(:opts)
  118. 2 Uploader = new(:uploader)
  119. # CarrierWave-specific fields
  120. 2 Model = new(:model)
  121. 2 MountPoint = new(:mount_point)
  122. 2 Version = new(:version)
  123. 2 StorePath = new(:store_path)
  124. 2 Extension = new(:ext)
  125. # SQL-specific fields
  126. 2 Sql = new(:sql)
  127. 2 Name = new(:name)
  128. 2 RowCount = new(:row_count)
  129. # Use Adapter for both AMS and SQL adapter name
  130. 2 BindParams = new(:bind_params)
  131. 2 DatabaseName = new(:db_name)
  132. 2 ConnectionPoolSize = new(:pool_size)
  133. 2 ActiveConnections = new(:active_count)
  134. 2 OperationType = new(:op_type)
  135. 2 TableNames = new(:table_names)
  136. # ActiveModelSerializers fields
  137. 2 Serializer = new(:serializer)
  138. 2 Adapter = new(:adapter)
  139. 2 ResourceClass = new(:resource_class)
  140. # Ahoy-specific fields
  141. 2 AhoyEvent = new(:ahoy_event)
  142. 2 Properties = new(:properties)
  143. # Puma / server lifecycle fields
  144. 2 Mode = new(:mode)
  145. 2 PumaVersion = new(:puma_version)
  146. 2 PumaCodename = new(:puma_codename)
  147. 2 RubyVersion = new(:ruby_version)
  148. 2 MinThreads = new(:min_threads)
  149. 2 MaxThreads = new(:max_threads)
  150. 2 Environment = new(:environment)
  151. 2 ListeningAddresses = new(:listening_addresses)
  152. 2 Address = new(:addr)
  153. end
  154. end
  155. end

lib/log_struct/enums/source.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Combined Source class that unifies log and error sources
  5. 2 class Source < T::Enum
  6. 2 enums do
  7. # Error sources
  8. 2 TypeChecking = new(:type_checking) # For type checking errors (Sorbet)
  9. 2 Security = new(:security) # Security-related events
  10. # Errors from LogStruct. (Cannot use LogStruct here because it confuses tapioca.)
  11. 2 Internal = new(:logstruct)
  12. # Application sources
  13. 2 Rails = new(:rails) # For request-related logs/errors
  14. 2 Job = new(:job) # ActiveJob logs/errors
  15. 2 Storage = new(:storage) # ActiveStorage logs/errors
  16. 2 Mailer = new(:mailer) # ActionMailer logs/errors
  17. 2 App = new(:app) # General application logs/errors
  18. # Third-party gem sources
  19. 2 Shrine = new(:shrine)
  20. 2 CarrierWave = new(:carrierwave)
  21. 2 Sidekiq = new(:sidekiq)
  22. 2 Dotenv = new(:dotenv)
  23. 2 Puma = new(:puma)
  24. end
  25. end
  26. end

lib/log_struct/formatter.rb

96.8% lines covered

219 relevant lines. 212 lines covered and 7 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "logger"
  4. 2 require "active_support/core_ext/object/blank"
  5. 2 require "active_support/hash_with_indifferent_access"
  6. 2 require "json"
  7. 2 require "globalid"
  8. 2 require_relative "enums/source"
  9. 2 require_relative "enums/event"
  10. 2 require_relative "enums/log_field"
  11. 2 require_relative "string_scrubber"
  12. 2 require_relative "log"
  13. 2 require_relative "param_filters"
  14. 2 require_relative "multi_error_reporter"
  15. 2 module LogStruct
  16. 2 class Formatter < ::Logger::Formatter
  17. 2 extend T::Sig
  18. # Add current_tags method to support ActiveSupport::TaggedLogging
  19. 3 sig { returns(T::Array[String]) }
  20. 2 def current_tags
  21. 7 Thread.current[:activesupport_tagged_logging_tags] ||= []
  22. end
  23. # Add tagged method to support ActiveSupport::TaggedLogging
  24. 3 sig { params(tags: T::Array[String], blk: T.proc.params(formatter: Formatter).void).returns(T.untyped) }
  25. 2 def tagged(*tags, &blk)
  26. 1 new_tags = tags.flatten
  27. 1 current_tags.concat(new_tags) if new_tags.any?
  28. 1 yield self
  29. ensure
  30. 1 current_tags.pop(new_tags.size) if new_tags&.any?
  31. end
  32. # Add clear_tags! method to support ActiveSupport::TaggedLogging
  33. 3 sig { void }
  34. 2 def clear_tags!
  35. 1 Thread.current[:activesupport_tagged_logging_tags] = []
  36. end
  37. 2 sig { params(tags: T::Array[String]).returns(T.untyped) }
  38. 2 def push_tags(*tags)
  39. current_tags.concat(tags)
  40. end
  41. 4 sig { params(string: String).returns(String) }
  42. 2 def scrub_string(string)
  43. # Use StringScrubber module to scrub sensitive information from strings
  44. 12773 StringScrubber.scrub(string)
  45. end
  46. 2 MAX_RECURSION_DEPTH = 20
  47. # The mask Rails' parameter filter puts in place of filtered values
  48. 2 RAILS_FILTERED_MASK = "[FILTERED]"
  49. # LogStruct's own fields, which some filters don't apply to at the top of a line
  50. 2 LOG_FIELD_KEYS = T.let(LogField.values.map(&:serialize).to_set.freeze, T::Set[T.untyped])
  51. # path and root are for filters that match a key by its path (see ParamFilters.should_filter_key?).
  52. # They're only tracked when such filters exist: path is nil otherwise.
  53. 2 sig do
  54. 2 params(
  55. arg: T.untyped,
  56. recursion_depth: Integer,
  57. ancestors: T::Hash[T.untyped, T::Boolean],
  58. path: T.nilable(T::Array[String]),
  59. root: T.untyped
  60. ).returns(T.untyped)
  61. end
  62. 2 def process_values(arg, recursion_depth: 0, ancestors: {}.compare_by_identity, path: nil, root: nil)
  63. 17305 if recursion_depth == 0 && path.nil? && !LogStruct.config.filters.path_matchers.empty?
  64. 4 path = []
  65. end
  66. # Circular references and deeply nested values stop here. Values below this depth
  67. # would skip filtering and scrubbing, so they're summarized instead of logged.
  68. 17305 return summarize_unprocessed_value(arg) if recursion_depth > MAX_RECURSION_DEPTH || ancestors.key?(arg)
  69. 17292 case arg
  70. when Hash, Array
  71. 3881 ancestors[arg] = true
  72. begin
  73. 3881 if arg.is_a?(Hash)
  74. 3584 process_hash(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
  75. else
  76. 297 process_array(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
  77. end
  78. ensure
  79. 3881 ancestors.delete(arg)
  80. end
  81. when GlobalID::Identification
  82. # Scrubbed like any String: a record's ID can be an email address
  83. begin
  84. 22 scrub_string(arg.to_global_id.to_s)
  85. rescue
  86. begin
  87. 1 object = T.cast(arg, Object)
  88. # Active Record is optional
  89. 1 if defined?(::ActiveRecord::Base) && object.is_a?(::ActiveRecord::Base)
  90. scrub_string("#{object.class}(##{object.id})")
  91. else
  92. # For non-ActiveRecord objects that failed to_global_id, try to get a string representation
  93. # If this also fails, we want to catch it and return the error placeholder
  94. 1 scrub_string(String(object))
  95. end
  96. rescue => e
  97. 1 LogStruct.handle_exception(e, source: Source::Internal)
  98. 1 "[GLOBALID_ERROR]"
  99. end
  100. end
  101. when Source, Event
  102. arg.serialize
  103. when String
  104. 12752 scrub_limited_string(valid_utf8(arg))
  105. when Time
  106. arg.iso8601(3)
  107. when Symbol
  108. 115 StringScrubber.scrub_name(arg)
  109. when Float
  110. # JSON has no NaN or Infinity
  111. 48 arg.finite? ? arg : arg.to_s
  112. when Numeric, TrueClass, FalseClass, NilClass
  113. 343 arg
  114. else
  115. # Structs, ActionController::Parameters and other objects: filter and scrub what
  116. # to_json would write
  117. 131 process_values(arg.as_json, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: path, root: root)
  118. end
  119. rescue SystemStackError, StandardError => e
  120. # A SystemStackError comes from an object whose as_json refers back to itself
  121. 2 e = RuntimeError.new("#{e.class}: #{e.message}") unless e.is_a?(StandardError)
  122. # Report error through LogStruct's framework
  123. 2 context = {
  124. processor_method: "process_values",
  125. value_type: arg.class.name,
  126. recursion_depth: recursion_depth
  127. }
  128. 2 LogStruct.handle_exception(e, source: Source::Internal, context: context)
  129. 2 summarize_unprocessed_value(arg)
  130. end
  131. 2 sig do
  132. 2 params(
  133. hash: T::Hash[T.untyped, T.untyped],
  134. recursion_depth: Integer,
  135. ancestors: T::Hash[T.untyped, T::Boolean],
  136. path: T.nilable(T::Array[String]),
  137. root: T.untyped
  138. ).returns(T::Hash[T.untyped, T.untyped])
  139. end
  140. 2 def process_hash(hash, recursion_depth:, ancestors:, path: nil, root: nil)
  141. 3584 result = {}
  142. # Path matchers' roots are built only if a matcher reads them (most don't)
  143. 3584 line_root = (path && recursion_depth == 0) ? ConfigStruct::LazyRoot.new(hash) : nil
  144. 3584 max_keys = LogStruct.config.limits.max_hash_keys
  145. 3584 hash.each_with_index do |(key, value), index|
  146. 12187 if index >= max_keys
  147. 3 result[:_more_keys] = hash.size - max_keys
  148. 3 break
  149. end
  150. 12184 key = valid_utf8_key(key)
  151. 12184 logstruct_field = recursion_depth == 0 && LOG_FIELD_KEYS.include?(key)
  152. 12184 key_path = path && [*path, key.to_s]
  153. # A top-level key's root is the line; the keys below it get the top-level field's value
  154. 12184 key_root = (recursion_depth == 0) ? line_root : root
  155. 12184 children_root = (path && recursion_depth == 0) ? ConfigStruct::LazyRoot.new(value) : root
  156. # Check if this key should be filtered at any depth
  157. 12184 filtered = ParamFilters.should_filter_key?(key, value, logstruct_field: logstruct_field, path: key_path, root: key_root)
  158. # Keys are scrubbed too: an email address can be a key
  159. 12184 result[StringScrubber.scrub_name(key)] = if !filtered
  160. 12094 process_values(value, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: key_path, root: children_root)
  161. 90 elsif value == RAILS_FILTERED_MASK
  162. # Rails' parameter filter has already replaced the value
  163. 9 value
  164. else
  165. 81 {_filtered: ParamFilters.summarize_json_attribute(key, value)}
  166. end
  167. end
  168. 3583 result
  169. end
  170. # The string as valid UTF-8. JSON can't hold other bytes: Puma passes headers as binary
  171. # strings, and a byte like \xFF in a User-Agent would stop the whole line from being written.
  172. # Bytes that aren't valid UTF-8 become U+FFFD.
  173. 4 sig { params(string: String).returns(String) }
  174. 2 def valid_utf8(string)
  175. 24936 StringScrubber.valid_utf8(string)
  176. end
  177. # A String or Symbol key as valid UTF-8 (symbolize_keys makes Symbols of binary String keys)
  178. 4 sig { params(key: T.untyped).returns(T.untyped) }
  179. 2 def valid_utf8_key(key)
  180. 12184 case key
  181. when String
  182. 28 valid_utf8(key)
  183. when Symbol
  184. 12156 name = key.name
  185. 12156 fixed = valid_utf8(name)
  186. 12156 fixed.equal?(name) ? key : fixed.to_sym
  187. else
  188. key
  189. end
  190. end
  191. # The scrubbers see a little more than a cut string keeps, so data at the cut is scrubbed whole
  192. 2 SCRUB_MARGIN_BYTES = 256
  193. # A string scrubbed, and cut to limits.max_string_bytes. The scrubbers see its first max_string_bytes plus
  194. # SCRUB_MARGIN_BYTES: a value running past that was only partly seen, so scrub_cut_end replaces it if it can
  195. # be a credential.
  196. 4 sig { params(string: String).returns(String) }
  197. 2 def scrub_limited_string(string)
  198. 12752 max_bytes = LogStruct.config.limits.max_string_bytes
  199. 12752 return scrub_string(string) if string.bytesize <= max_bytes
  200. 12 seen_bytes = max_bytes + SCRUB_MARGIN_BYTES
  201. 12 scrubbed = scrub_string(string.byteslice(0, seen_bytes).to_s.scrub(""))
  202. 12 if string.bytesize > seen_bytes
  203. 6 scrubbed = StringScrubber.scrub_cut_end(scrubbed)
  204. 6 elsif scrubbed.bytesize <= max_bytes
  205. # The scrubbers saw all of it, and their replacements made it short enough
  206. 1 return scrubbed
  207. end
  208. 11 kept = scrubbed.byteslice(0, max_bytes).to_s.scrub("")
  209. 11 "#{kept}... (cut from #{string.bytesize} bytes)"
  210. end
  211. # Like deep_symbolize_keys, but stops at circular references and at the depth where
  212. # process_values stops, leaving those values for process_values to summarize
  213. 3 sig { params(value: T.untyped, recursion_depth: Integer, ancestors: T::Hash[T.untyped, T::Boolean]).returns(T.untyped) }
  214. 2 def symbolize_keys(value, recursion_depth: 0, ancestors: {}.compare_by_identity)
  215. 746 return value if recursion_depth > MAX_RECURSION_DEPTH || ancestors.key?(value)
  216. 740 case value
  217. when Hash
  218. 147 ancestors[value] = true
  219. 147 result = value.each_with_object({}) do |(key, nested_value), symbolized|
  220. 344 symbol_key = key.respond_to?(:to_sym) ? key.to_sym : key
  221. 344 symbolized[symbol_key] = symbolize_keys(nested_value, recursion_depth: recursion_depth + 1, ancestors: ancestors)
  222. end
  223. 147 ancestors.delete(value)
  224. 147 result
  225. when Array
  226. 12 ancestors[value] = true
  227. 310 result = value.map { |item| symbolize_keys(item, recursion_depth: recursion_depth + 1, ancestors: ancestors) }
  228. 12 ancestors.delete(value)
  229. 12 result
  230. else
  231. 581 value
  232. end
  233. end
  234. # Describes a value without filtering, scrubbing or serializing it, so that values which
  235. # couldn't be processed are never logged raw. Hashes and arrays are not serialized because
  236. # they may contain circular references.
  237. 3 sig { params(arg: T.untyped).returns(T.untyped) }
  238. 2 def summarize_unprocessed_value(arg)
  239. 15 case arg
  240. when Hash
  241. 10 {_filtered: {_class: arg.class.name, _keys_count: arg.size}}
  242. when Array
  243. 1 {_filtered: {_class: arg.class.name, _count: arg.size}}
  244. when String
  245. 1 {_filtered: {_class: arg.class.name}}
  246. when Numeric, Symbol, TrueClass, FalseClass, NilClass
  247. 2 arg
  248. else
  249. 1 {_filtered: {_class: arg.class.name}}
  250. end
  251. end
  252. 4 sig { params(log_value: T.untyped, time: Time).returns(T::Hash[Symbol, T.untyped]) }
  253. 2 def log_value_to_hash(log_value, time:)
  254. 1760 case log_value
  255. when Log::Interfaces::CommonFields
  256. # Our log classes all implement a custom #serialize method that use symbol keys
  257. 1674 log_value.serialize
  258. when T::Struct
  259. # Default T::Struct.serialize methods returns a hash with string keys, so convert them to symbols
  260. 2 symbolize_keys(log_value.serialize)
  261. when Hash
  262. # Use hash as is and convert string keys to symbols
  263. 74 symbolize_keys(log_value)
  264. when Exception
  265. # logger.error(exception): its class, message and backtrace
  266. 2 Log.from_exception(Source::App, log_value, {}, time).serialize
  267. else
  268. # Create a Plain log with the message as a string and serialize it with symbol keys
  269. # log_value can be literally anything: Integer, Float, Boolean, NilClass, etc.
  270. 8 log_message = case log_value
  271. # Handle all the basic types without any further processing
  272. when String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash, Time, Numeric
  273. 5 log_value
  274. else
  275. # Handle the serialization of complex objects in a useful way:
  276. #
  277. # 1. For ActiveRecord models: Use as_json which includes attributes
  278. # 2. For objects with custom as_json implementations: Use their implementation
  279. # 3. For basic objects that only have ActiveSupport's as_json: Use to_s
  280. begin
  281. 3 method_owner = log_value.method(:as_json).owner
  282. # If it's ActiveRecord, ActiveModel, or a custom implementation, use as_json
  283. 2 if method_owner.to_s.include?("ActiveRecord") ||
  284. method_owner.to_s.include?("ActiveModel") ||
  285. method_owner.to_s.exclude?("ActiveSupport::CoreExtensions") &&
  286. method_owner.to_s.exclude?("Object")
  287. 1 log_value.as_json
  288. else
  289. # For plain objects with only the default ActiveSupport as_json
  290. 1 log_value.to_s
  291. end
  292. rescue => e
  293. # Handle serialization errors
  294. context = {
  295. 1 object_class: log_value.class.name,
  296. object_inspect: log_value.inspect.truncate(100)
  297. }
  298. 1 LogStruct.handle_exception(e, source: Source::Internal, context: context)
  299. # Fall back to the string representation to ensure we continue processing
  300. 1 log_value.to_s
  301. end
  302. end
  303. 8 Log::Plain.new(
  304. message: log_message,
  305. timestamp: time
  306. ).serialize
  307. end
  308. end
  309. # Serializes Log (or string) into JSON.
  310. # request_id: the request's ID (default: the one RequestContext stored for this thread).
  311. # context: fields for the line's ctx, like the running job's ID and class. The line's own ctx keys win.
  312. # tags: the logger's tags (logger.tagged("billing")), written as tags unless the line has its own.
  313. 2 sig do
  314. 2 params(
  315. severity: T.any(String, Symbol, Integer),
  316. time: Time,
  317. progname: T.nilable(String),
  318. log_value: T.untyped,
  319. request_id: T.nilable(String),
  320. context: T.nilable(T::Hash[T.untyped, T.untyped]),
  321. tags: T.nilable(T::Array[T.untyped])
  322. ).returns(String)
  323. end
  324. 2 def call(severity, time, progname, log_value, request_id: nil, context: nil, tags: nil)
  325. 1753 level_enum = Level.from_severity(severity)
  326. 1753 data = log_value_to_hash(log_value, time: time)
  327. # Filter params, scrub sensitive values, format ActiveJob GlobalID arguments
  328. 1753 data = process_values(data)
  329. # Add standard fields if not already present
  330. 1753 data[:src] ||= Source::App
  331. 1753 data[:evt] ||= Event::Log
  332. 1753 data[:ts] ||= time.iso8601(3)
  333. 1753 data[:lvl] = level_enum # Set level from severity parameter
  334. 1753 add_metadata(data, progname: progname, request_id: request_id || Thread.current[:logstruct_request_id], context: context, tags: tags)
  335. begin
  336. 1753 generate_json(data)
  337. rescue SystemStackError, StandardError => e
  338. 1 unwritable_line_json(data, e)
  339. end
  340. end
  341. # A line that can't be written as JSON is replaced by one that says so, instead of being lost
  342. 3 sig { params(data: T::Hash[T.untyped, T.untyped], error: Exception).returns(String) }
  343. 2 def unwritable_line_json(data, error)
  344. 1 level = data[:lvl]
  345. line = {
  346. 1 src: enum_or_string(data[:src]) || Source::Internal.serialize,
  347. evt: enum_or_string(data[:evt]) || Event::Error.serialize,
  348. 1 ts: data[:ts].is_a?(String) ? data[:ts] : Time.now.iso8601(3),
  349. 1 lvl: level.is_a?(Level) ? level.serialize : Level::Error.serialize,
  350. msg: "LogStruct couldn't write this line as JSON",
  351. error_class: error.class.name
  352. }
  353. 1 unless Thread.current[:logstruct_reporting_unwritable_line]
  354. begin
  355. 1 Thread.current[:logstruct_reporting_unwritable_line] = true
  356. 1 reported = error.is_a?(StandardError) ? error : RuntimeError.new("#{error.class}: #{error.message}")
  357. 1 LogStruct.handle_exception(reported, source: Source::Internal, context: {src: line[:src], evt: line[:evt]})
  358. ensure
  359. 1 Thread.current[:logstruct_reporting_unwritable_line] = nil
  360. end
  361. end
  362. 1 "#{::JSON.generate(line)}\n"
  363. end
  364. # The program name, request ID and job context, filtered and scrubbed like the rest of the line:
  365. # an X-Request-Id header can carry anything
  366. 2 sig do
  367. 2 params(
  368. data: T::Hash[T.untyped, T.untyped],
  369. progname: T.nilable(String),
  370. request_id: T.untyped,
  371. context: T.nilable(T::Hash[T.untyped, T.untyped]),
  372. tags: T.nilable(T::Array[T.untyped])
  373. ).void
  374. end
  375. 2 def add_metadata(data, progname:, request_id:, context:, tags:)
  376. 1753 metadata = {}
  377. 1753 metadata[:prog] = progname if progname.present?
  378. 1753 metadata[:tags] = tags if tags.is_a?(Array) && !tags.empty? && !data.key?(:tags)
  379. 1753 if request_id.is_a?(String) && !request_id.empty? && !data.key?(:req_id)
  380. 51 metadata[:req_id] = request_id
  381. end
  382. 1753 metadata[:ctx] = symbolize_keys(context) if context.present?
  383. 1753 return if metadata.empty?
  384. 1663 processed = process_values(metadata)
  385. 1663 job_context = processed.delete(:ctx)
  386. 1663 data.merge!(processed)
  387. 1663 return if job_context.nil?
  388. 28 line_context = data[:ctx]
  389. # A logged Hash's own ctx keeps its keys and gains the job's
  390. 28 data[:ctx] = line_context.is_a?(Hash) ? job_context.merge(line_context) : (line_context || job_context)
  391. end
  392. 3 sig { params(value: T.untyped).returns(T.nilable(String)) }
  393. 2 def enum_or_string(value)
  394. 2 if value.is_a?(Source) || value.is_a?(Event)
  395. 2 value.serialize.to_s
  396. elsif value.is_a?(String)
  397. value
  398. end
  399. end
  400. # Output as JSON with a newline. We mock this method in tests so we can
  401. # inspect the data right before it gets turned into a JSON string.
  402. 4 sig { params(data: T::Hash[T.untyped, T.untyped]).returns(String) }
  403. 2 def generate_json(data)
  404. 1753 "#{data.to_json}\n"
  405. end
  406. # An array's items keep the array's path, as in Rails (users.name filters each user's name)
  407. 2 sig do
  408. 2 params(
  409. array: T::Array[T.untyped],
  410. recursion_depth: Integer,
  411. ancestors: T::Hash[T.untyped, T::Boolean],
  412. path: T.nilable(T::Array[String]),
  413. root: T.untyped
  414. ).returns(T::Array[T.untyped])
  415. end
  416. 2 def process_array(array, recursion_depth:, ancestors:, path: nil, root: nil)
  417. 297 return [] if array.empty?
  418. 270 limits = LogStruct.config.limits
  419. 270 backtrace = looks_like_backtrace_array?(array)
  420. 270 max_items = backtrace ? limits.max_backtrace_lines : limits.max_array_items
  421. 270 processed = array.first(max_items).map do |value|
  422. 1619 process_values(value, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: path, root: root)
  423. end
  424. 270 if array.size > max_items
  425. 66 processed << "... and #{array.size - max_items} more #{backtrace ? "lines" : "items"}"
  426. end
  427. 270 processed
  428. end
  429. # Check if an array looks like a backtrace (array of strings with file:line pattern)
  430. 3 sig { params(array: T::Array[T.untyped]).returns(T::Boolean) }
  431. 2 def looks_like_backtrace_array?(array)
  432. 270 backtrace_like_count = array.first(5).count do |element|
  433. 678 element.is_a?(String) && element.match?(/\A[^:\s]+:\d+/)
  434. end
  435. 270 backtrace_like_count >= 3
  436. end
  437. end
  438. end

lib/log_struct/handlers.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Module for custom handlers used throughout the library
  5. 2 module Handlers
  6. # Type for Lograge custom options
  7. 2 LogrageCustomOptions = T.type_alias {
  8. 2 T.proc.params(
  9. event: ActiveSupport::Notifications::Event,
  10. options: T::Hash[Symbol, T.untyped]
  11. ).returns(T.untyped)
  12. }
  13. # Type for error reporting handlers
  14. 2 ErrorReporter = T.type_alias {
  15. 2 T.proc.params(
  16. error: StandardError,
  17. context: T.nilable(T::Hash[Symbol, T.untyped]),
  18. source: Source
  19. ).void
  20. }
  21. # Type for string scrubbing handlers
  22. 4 StringScrubber = T.type_alias { T.proc.params(string: String).returns(String) }
  23. end
  24. end

lib/log_struct/hash_utils.rb

95.65% lines covered

23 relevant lines. 22 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "openssl"
  4. 2 module LogStruct
  5. # Utility module for hashing sensitive data
  6. 2 module HashUtils
  7. # The key when there's neither a configured hash_salt nor a Rails app with a secret_key_base.
  8. # It's public, so these hashes can be matched to a list of candidate values.
  9. 2 FALLBACK_KEY = "l0g5t0p"
  10. 2 class << self
  11. 2 extend T::Sig
  12. # An HMAC-SHA256 of the value, so lines about the same email can be traced without logging it
  13. 3 sig { params(value: String).returns(String) }
  14. 2 def hash_value(value)
  15. 350 length = LogStruct.config.filters.hash_length
  16. 350 OpenSSL::HMAC.hexdigest("SHA256", hash_key, value)[0...length] || "error"
  17. end
  18. # The configured hash_salt, or a key derived from the app's secret_key_base
  19. 3 sig { returns(String) }
  20. 2 def hash_key
  21. 352 LogStruct.config.filters.hash_salt || derived_key || FALLBACK_KEY
  22. end
  23. # Forget the derived key (for tests that change the Rails app)
  24. 3 sig { void }
  25. 2 def reset!
  26. 6 @derived_key = T.let(nil, T.nilable(String))
  27. end
  28. 2 private
  29. 3 sig { returns(T.nilable(String)) }
  30. 2 def derived_key
  31. 5 @derived_key ||= T.let(begin
  32. 3 application = ::Rails.application if defined?(::Rails) && ::Rails.respond_to?(:application)
  33. # Rails derives keys like this one for cookies and messages; nil until the app has its secret
  34. 3 application&.key_generator&.generate_key("LogStruct hashed values", 32)
  35. rescue
  36. nil
  37. end,
  38. T.nilable(String))
  39. end
  40. end
  41. end
  42. end

lib/log_struct/integrations.rb

95.0% lines covered

60 relevant lines. 57 lines covered and 3 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "integrations/integration_interface"
  4. 2 require_relative "integrations/rails_log_subscribers"
  5. 2 require_relative "integrations/request_context"
  6. 2 require_relative "integrations/active_job"
  7. 2 require_relative "integrations/active_record"
  8. 2 require_relative "integrations/rack_error_handler"
  9. 2 require_relative "integrations/host_authorization"
  10. 2 require_relative "integrations/action_mailer"
  11. 2 require_relative "integrations/lograge"
  12. 2 require_relative "integrations/shrine"
  13. 2 require_relative "integrations/sidekiq"
  14. 2 require_relative "integrations/good_job"
  15. 2 require_relative "integrations/active_storage"
  16. 2 require_relative "integrations/carrierwave"
  17. 2 require_relative "integrations/sorbet"
  18. 2 require_relative "integrations/ahoy"
  19. 2 require_relative "integrations/active_model_serializers"
  20. 2 require_relative "integrations/dotenv"
  21. 2 require_relative "integrations/puma"
  22. 2 module LogStruct
  23. 2 module Integrations
  24. 2 extend T::Sig
  25. # Register generic initializers on the Railtie to keep integration
  26. # wiring centralized (boot replay interception and resolution).
  27. 4 sig { params(railtie: T.untyped).void }
  28. 2 def self.setup_initializers(railtie)
  29. # Intercept any boot-time replays (e.g., dotenv) before those railties run
  30. 2 railtie.initializer "logstruct.intercept_boot_replays", before: "dotenv" do
  31. 2 LogStruct::Integrations::Dotenv.intercept_logger_setter!
  32. end
  33. # Decide which set of boot logs to emit once `enabled` is final
  34. 2 railtie.initializer "logstruct.resolve_boot_logs", after: "logstruct.setup" do
  35. 2 LogStruct::Integrations::Dotenv.resolve_boot_logs!
  36. end
  37. end
  38. 4 sig { params(stage: Symbol).void }
  39. 2 def self.setup_integrations(stage: :all)
  40. 4 config = LogStruct.config
  41. 4 case stage
  42. when :non_middleware
  43. 2 setup_non_middleware_integrations(config)
  44. when :middleware
  45. 2 setup_middleware_integrations(config)
  46. when :all
  47. setup_non_middleware_integrations(config)
  48. setup_middleware_integrations(config)
  49. else
  50. raise ArgumentError, "Unknown integration stage: #{stage}"
  51. end
  52. end
  53. 4 sig { params(config: LogStruct::Configuration).void }
  54. 2 def self.setup_non_middleware_integrations(config)
  55. 2 Integrations::Lograge.setup(config) if config.integrations.enable_lograge
  56. 2 Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
  57. 2 Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
  58. 2 Integrations::ActiveRecord.setup(config) if config.integrations.enable_sql_logging
  59. 2 Integrations::Sidekiq.setup(config) if config.integrations.enable_sidekiq
  60. 2 Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
  61. 2 Integrations::Ahoy.setup(config) if config.integrations.enable_ahoy
  62. 2 Integrations::ActiveModelSerializers.setup(config) if config.integrations.enable_active_model_serializers
  63. 2 Integrations::Shrine.setup(config) if config.integrations.enable_shrine
  64. 2 Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
  65. 2 Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
  66. 2 Integrations::Sorbet.setup(config) if config.integrations.enable_sorbet_error_handlers
  67. 2 if config.enabled && config.integrations.enable_dotenv
  68. 2 Integrations::Dotenv.setup(config)
  69. end
  70. 2 Integrations::Puma.setup(config) if config.integrations.enable_puma
  71. end
  72. 4 sig { params(config: LogStruct::Configuration).void }
  73. 2 def self.setup_middleware_integrations(config)
  74. 2 Integrations::RequestContext.setup(config)
  75. 2 Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
  76. 2 Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
  77. end
  78. 2 private_class_method :setup_non_middleware_integrations, :setup_middleware_integrations
  79. end
  80. end

lib/log_struct/integrations/action_mailer.rb

90.0% lines covered

30 relevant lines. 27 lines covered and 3 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "action_mailer"
  5. rescue LoadError
  6. # actionmailer gem is not available, integration will be skipped
  7. end
  8. 2 if defined?(::ActionMailer)
  9. 2 require_relative "action_mailer/metadata_collection"
  10. 2 require_relative "action_mailer/event_logging"
  11. 2 require_relative "action_mailer/error_handling"
  12. 2 require_relative "action_mailer/fallback_error_handlers"
  13. end
  14. 2 module LogStruct
  15. 2 module Integrations
  16. # ActionMailer integration for structured logging
  17. 2 module ActionMailer
  18. 2 extend T::Sig
  19. 2 extend IntegrationInterface
  20. # Set up ActionMailer structured logging
  21. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  22. 2 def self.setup(config)
  23. 10 return nil unless defined?(::ActionMailer)
  24. 10 return nil unless config.enabled
  25. 10 return nil unless config.integrations.enable_actionmailer
  26. # Register our custom observers and handlers on all mailers. The hook runs right away
  27. # if ActionMailer::Base is already loaded.
  28. 9 ActiveSupport.on_load(:action_mailer) do
  29. # LogStruct's delivery lines replace Rails' mail lines, which also log the whole mail at debug level.
  30. # Mailers keep their logger, so an app's own logger calls in a mailer are still logged.
  31. 9 RailsLogSubscribers.detach(::ActionMailer::LogSubscriber, :action_mailer) if defined?(::ActionMailer::LogSubscriber)
  32. 9 prepend LogStruct::Integrations::ActionMailer::EventLogging
  33. 9 prepend LogStruct::Integrations::ActionMailer::ErrorHandling
  34. 9 prepend LogStruct::Integrations::ActionMailer::MetadataCollection
  35. end
  36. 9 true
  37. end
  38. # Where LogStruct doesn't set up ActionMailer, mailers still get the error handlers they can name in
  39. # rescue_from (see FallbackErrorHandlers)
  40. 4 sig { params(config: LogStruct::Configuration).void }
  41. 2 def self.setup_fallback_error_handlers(config)
  42. 6 return unless defined?(::ActionMailer)
  43. 6 return if config.enabled && config.integrations.enable_actionmailer
  44. 2 ActiveSupport.on_load(:action_mailer) do
  45. next if include?(LogStruct::Integrations::ActionMailer::FallbackErrorHandlers)
  46. include LogStruct::Integrations::ActionMailer::FallbackErrorHandlers
  47. extend LogStruct::Integrations::ActionMailer::FallbackErrorHandlers::ClassMethods
  48. end
  49. end
  50. end
  51. end
  52. end

lib/log_struct/integrations/action_mailer/error_handling.rb

88.03% lines covered

117 relevant lines. 103 lines covered and 14 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module ActionMailer
  6. # Handles error handling for ActionMailer
  7. #
  8. # IMPORTANT LIMITATIONS:
  9. # 1. This module must be included BEFORE users define rescue_from handlers
  10. # to ensure proper handler precedence (user handlers are checked first)
  11. # 2. Rails rescue_from handlers don't bubble to parent class handlers after reraise
  12. # 3. Handler order matters: Rails checks rescue_from handlers in reverse declaration order
  13. 2 module ErrorHandling
  14. 2 extend T::Sig
  15. 2 extend ActiveSupport::Concern
  16. # ActionMailer::MailDeliveryJob passes errors to the mailer class (`handle_exception`), which
  17. # looks the rescue_from handlers up on the class, so the handlers are defined there too.
  18. # (ActiveSupport::Concern extends the class with ClassMethods when it's included or prepended.)
  19. 2 module ClassMethods
  20. 2 extend T::Sig
  21. # Logs the error, and doesn't report or raise it
  22. 3 sig { params(ex: StandardError).void }
  23. 2 def log_and_ignore_error(ex)
  24. 4 ErrorHandling.handle_mailer_class_error(self, ex, report: false, reraise: false)
  25. end
  26. # Logs and reports the error, and doesn't raise it
  27. 3 sig { params(ex: StandardError).void }
  28. 2 def log_and_report_error(ex)
  29. 7 ErrorHandling.handle_mailer_class_error(self, ex, report: true, reraise: false)
  30. end
  31. # Logs the error and raises it again, reporting it too in the Report modes
  32. 3 sig { params(ex: StandardError).void }
  33. 2 def log_and_reraise_error(ex)
  34. 5 ErrorHandling.handle_mailer_class_error(self, ex, report: ErrorHandling.report_before_reraise?, reraise: true)
  35. end
  36. end
  37. # log_and_reraise_error reports the error where standard_errors' mode reports the app's errors
  38. # (Report, and ReportProduction in production). Elsewhere whatever rescues the raised error decides.
  39. 3 sig { returns(T::Boolean) }
  40. 2 def self.report_before_reraise?
  41. 24 mode = LogStruct.error_handling_mode_for(Source::Mailer)
  42. 24 mode == ErrorHandlingMode::Report || (mode == ErrorHandlingMode::ReportProduction && LogStruct.is_production?)
  43. end
  44. # An error that reaches the class handler from the delivery itself was logged (and reported)
  45. # by the mailer's handler, which raised it again (log_and_reraise_error). It's raised again,
  46. # without logging it twice, so the job fails as deliver_now would.
  47. 3 sig { params(mailer_class: T.untyped, error: StandardError, report: T::Boolean, reraise: T::Boolean).void }
  48. 2 def self.handle_mailer_class_error(mailer_class, error, report:, reraise:)
  49. 16 Kernel.raise error if LogStruct::LoggedErrors.logged?(error)
  50. 13 message = if reraise
  51. 2 "#{error.class}: Email delivery failed. Error message: #{error.message}"
  52. else
  53. 11 "#{error.class}: Cannot send email. Error message: #{error.message}"
  54. end
  55. 13 LogStruct.error(Log::ActionMailer::Error.new(
  56. mailer_class: mailer_class.to_s,
  57. error_class: error.class,
  58. message: message,
  59. backtrace: error.backtrace,
  60. level: Level::Error
  61. ))
  62. 13 LogStruct.report_error(error, source: Source::Mailer, context: {mailer_class: mailer_class.to_s}) if report
  63. 13 Kernel.raise error if reraise
  64. end
  65. 3 sig { returns(T.nilable(T::Boolean)) }
  66. 2 attr_accessor :logstruct_mail_failed
  67. # NOTE: rescue_from handlers are checked in reverse order of declaration.
  68. # We want LogStruct handlers to be checked AFTER user handlers (lower priority),
  69. # so we need to add them BEFORE user handlers are declared.
  70. # This will be called when the module is included/prepended
  71. 4 sig { params(base: T.untyped).void }
  72. 2 def self.install_handler(base)
  73. # Only add the handler once per class
  74. 10 return if base.instance_variable_get(:@_logstruct_handler_installed)
  75. # Add our handler FIRST so it has lower priority than user handlers
  76. 3 base.rescue_from StandardError, with: :log_and_reraise_error
  77. # Mark as installed to prevent duplicates
  78. 3 base.instance_variable_set(:@_logstruct_handler_installed, true)
  79. end
  80. 2 included do
  81. LogStruct::Integrations::ActionMailer::ErrorHandling.install_handler(self)
  82. end
  83. # Also support prepended (used by tests and manual setup)
  84. 4 sig { params(base: T.untyped).void }
  85. 2 def self.prepended(base)
  86. 10 install_handler(base)
  87. end
  88. 2 protected
  89. # Logs the error, and doesn't report or raise it
  90. 3 sig { params(ex: StandardError).void }
  91. 2 def log_and_ignore_error(ex)
  92. 8 self.logstruct_mail_failed = true
  93. 8 log_email_delivery_error(ex, notify: false, report: false, reraise: false)
  94. end
  95. # Logs and reports the error, and doesn't raise it
  96. 3 sig { params(ex: StandardError).void }
  97. 2 def log_and_report_error(ex)
  98. 14 log_email_delivery_error(ex, notify: false, report: true, reraise: false)
  99. end
  100. # Logs the error and raises it again, reporting it too in the Report modes (report_before_reraise?)
  101. 3 sig { params(ex: StandardError).void }
  102. 2 def log_and_reraise_error(ex)
  103. 18 log_email_delivery_error(ex, notify: false, report: ErrorHandling.report_before_reraise?, reraise: true)
  104. end
  105. 2 private
  106. # Handle an error from a mailer
  107. 3 sig { params(mailer: T.untyped, error: StandardError, message: String).void }
  108. 2 def log_structured_error(mailer, error, message)
  109. # Get message if available
  110. 40 mailer_message = mailer.respond_to?(:message) ? mailer.message : nil
  111. # Prepare universal mailer fields
  112. 40 message_data = {}
  113. 40 MetadataCollection.add_message_metadata(mailer, message_data)
  114. # Prepare app-specific context data for additional_data
  115. 40 context_data = {}
  116. 40 MetadataCollection.add_context_metadata(mailer, context_data)
  117. # Extract email fields
  118. 40 to = mailer_message&.to
  119. 40 from = mailer_message&.from&.first
  120. 40 subject = mailer_message&.subject
  121. 40 message_id = extract_message_id_from_mailer(mailer)
  122. # Create ActionMailer-specific error struct
  123. 40 exception_data = Log::ActionMailer::Error.new(
  124. to: to,
  125. from: from,
  126. subject: subject,
  127. message_id: message_id,
  128. mailer_class: mailer.class.to_s,
  129. 40 mailer_action: mailer.respond_to?(:action_name) ? mailer.action_name&.to_s : nil,
  130. attachment_count: message_data[:attachment_count],
  131. error_class: error.class,
  132. message: message,
  133. backtrace: error.backtrace,
  134. additional_data: context_data.presence,
  135. timestamp: Time.now,
  136. level: Level::Error
  137. )
  138. # Log the structured error
  139. 40 LogStruct.error(exception_data)
  140. end
  141. # Extract message ID from the mailer
  142. 3 sig { params(mailer: T.untyped).returns(T.nilable(String)) }
  143. 2 def extract_message_id_from_mailer(mailer)
  144. 40 return nil unless mailer.respond_to?(:message)
  145. 40 mail_message = mailer.message
  146. 40 return nil unless mail_message.respond_to?(:message_id)
  147. 40 mail_message.message_id
  148. end
  149. # Log when email delivery fails
  150. 3 sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
  151. 2 def log_email_delivery_error(error, notify: false, report: true, reraise: true)
  152. # Generate appropriate error message
  153. 40 message = error_message_for(error, reraise)
  154. # Use structured error logging
  155. 40 log_structured_error(self, error, message)
  156. # The mailer class handler gets the error if it's raised from here (re-raised, or raised by
  157. # LogStruct.handle_exception in Raise mode), and mustn't log or report it again
  158. 40 LogStruct::LoggedErrors.mark(error)
  159. # Handle notifications, reporting and raising
  160. 40 handle_error_notifications(error, notify, report, reraise)
  161. end
  162. # Generate appropriate error message based on error handling strategy
  163. 3 sig { params(error: StandardError, reraise: T::Boolean).returns(String) }
  164. 2 def error_message_for(error, reraise)
  165. 40 if reraise
  166. 18 "#{error.class}: Email delivery failed. Recipients: #{recipients(error)}. Error message: #{error.message}"
  167. else
  168. 22 "#{error.class}: Cannot send email to #{recipients(error)}. Error message: #{error.message}"
  169. end
  170. end
  171. # Handle error notifications, reporting, and reraising
  172. 3 sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
  173. 2 def handle_error_notifications(error, notify, report, reraise)
  174. # Log a notification event if requested
  175. 40 log_notification_event(error) if notify
  176. # Report to error reporting service if requested. The structured error line is already logged.
  177. 40 if report
  178. context = {
  179. 22 mailer_class: self.class.to_s,
  180. 22 mailer_action: respond_to?(:action_name) ? action_name : nil,
  181. recipients: recipients(error)
  182. }
  183. 22 LogStruct.report_error(error, source: Source::Mailer, context: context)
  184. end
  185. # Re-raise the error if requested
  186. 40 Kernel.raise error if reraise
  187. end
  188. # Log a notification event that can be picked up by external systems
  189. 2 sig { params(error: StandardError).void }
  190. 2 def log_notification_event(error)
  191. context_data = {
  192. mailer: self.class.to_s,
  193. action: action_name&.to_s,
  194. recipients: recipients(error)
  195. }
  196. exception_data = build_exception_data(error, Level::Info, context_data)
  197. # Log the error at info level since it's not a critical error
  198. LogStruct.info(exception_data)
  199. end
  200. 2 sig { params(error: StandardError, level: Level, context_data: T::Hash[Symbol, T.untyped]).returns(Log::ActionMailer::Error) }
  201. 2 def build_exception_data(error, level, context_data)
  202. mailer_message = respond_to?(:message) ? message : nil
  203. message_data = {}
  204. MetadataCollection.add_message_metadata(self, message_data)
  205. MetadataCollection.add_context_metadata(self, context_data)
  206. to = mailer_message&.to
  207. from = mailer_message&.from&.first
  208. subject = mailer_message&.subject
  209. message_id = extract_message_id_from_mailer(self)
  210. Log::ActionMailer::Error.new(
  211. to: to,
  212. from: from,
  213. subject: subject,
  214. message_id: message_id,
  215. mailer_class: self.class.to_s,
  216. mailer_action: respond_to?(:action_name) ? action_name&.to_s : nil,
  217. attachment_count: message_data[:attachment_count],
  218. error_class: error.class,
  219. message: error.message,
  220. backtrace: error.backtrace,
  221. additional_data: context_data.presence,
  222. timestamp: Time.now,
  223. level: level
  224. )
  225. end
  226. # The formatter scrubs the addresses in the line
  227. 3 sig { params(error: StandardError).returns(String) }
  228. 2 def recipients(error)
  229. # Some delivery errors (Postmark's, for example) have the recipients that failed
  230. 62 if error.respond_to?(:recipients) && T.unsafe(error).recipients.present?
  231. 3 return T.unsafe(error).recipients.join(", ")
  232. end
  233. # Otherwise the mail's own: to, cc and bcc (unknown when the mailer failed before it built the mail)
  234. 59 mail = respond_to?(:message) ? message : nil
  235. 59 destinations = mail.respond_to?(:destinations) ? Array(mail.destinations).compact : []
  236. 59 destinations.empty? ? "unknown" : destinations.join(", ")
  237. end
  238. end
  239. end
  240. end
  241. end

lib/log_struct/integrations/action_mailer/event_logging.rb

92.16% lines covered

51 relevant lines. 47 lines covered and 4 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module ActionMailer
  6. # Handles logging of email delivery events
  7. 2 module EventLogging
  8. 2 extend ActiveSupport::Concern
  9. 2 extend T::Sig
  10. 2 extend T::Helpers
  11. 2 requires_ancestor { ::ActionMailer::Base }
  12. 2 requires_ancestor { ErrorHandling }
  13. 2 included do
  14. T.bind(self, T.class_of(::ActionMailer::Base))
  15. # Add callbacks for delivery events
  16. before_deliver :log_email_delivery
  17. after_deliver :log_email_delivered
  18. end
  19. # When this module is prepended (our integration uses prepend), ensure callbacks are registered
  20. 2 if respond_to?(:prepended)
  21. 2 prepended do
  22. 2 T.bind(self, T.class_of(::ActionMailer::Base))
  23. # Add callbacks for delivery events
  24. 2 before_deliver :log_email_delivery
  25. 2 after_deliver :log_email_delivered
  26. end
  27. end
  28. 2 protected
  29. # Log when an email is about to be delivered
  30. 3 sig { void }
  31. 2 def log_email_delivery
  32. 45 log_mailer_event(Event::Delivery)
  33. end
  34. # Log when an email is delivered
  35. 3 sig { void }
  36. 2 def log_email_delivered
  37. # Don't log delivered event if the delivery failed (error was handled with log_and_ignore_error)
  38. 7 return if logstruct_mail_failed
  39. 6 log_mailer_event(Event::Delivered)
  40. end
  41. 2 private
  42. # Log a mailer event with the given event type
  43. 3 sig { params(event_type: LogStruct::Event, level: Symbol, additional_data: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
  44. 2 def log_mailer_event(event_type, level = :info, additional_data = {})
  45. # Get message (self refers to the mailer instance)
  46. 51 mailer_message = message if respond_to?(:message)
  47. # Prepare universal mailer fields
  48. 51 message_data = {}
  49. 51 MetadataCollection.add_message_metadata(self, message_data)
  50. # Prepare app-specific context data for additional_data
  51. 51 context_data = {}
  52. 51 MetadataCollection.add_context_metadata(self, context_data)
  53. 51 context_data.merge!(additional_data) if additional_data.present?
  54. # Extract email fields (these will be filtered if email_addresses=true)
  55. 51 to = mailer_message&.to
  56. 51 from = mailer_message&.from&.first
  57. 51 subject = mailer_message&.subject
  58. 51 base_fields = Log::ActionMailer::BaseFields.new(
  59. to: to,
  60. from: from,
  61. subject: subject,
  62. message_id: extract_message_id,
  63. mailer_class: self.class.to_s,
  64. mailer_action: action_name.to_s,
  65. attachment_count: message_data[:attachment_count]
  66. )
  67. 51 log = case event_type
  68. when Event::Delivery
  69. 45 Log::ActionMailer::Delivery.new(
  70. **base_fields.to_kwargs,
  71. additional_data: context_data.presence,
  72. timestamp: Time.now
  73. )
  74. when Event::Delivered
  75. 6 Log::ActionMailer::Delivered.new(
  76. **base_fields.to_kwargs,
  77. additional_data: context_data.presence,
  78. timestamp: Time.now
  79. )
  80. else
  81. return
  82. end
  83. 51 LogStruct.info(log)
  84. 51 log
  85. end
  86. # Extract message ID from the mailer
  87. 3 sig { returns(T.nilable(String)) }
  88. 2 def extract_message_id
  89. 51 return nil unless respond_to?(:message)
  90. 51 mail_message = message
  91. 51 return nil unless mail_message.respond_to?(:message_id)
  92. 51 mail_message.message_id
  93. end
  94. end
  95. end
  96. end
  97. end

lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module ActionMailer
  6. # The error handlers a mailer can name in rescue_from (with: :log_and_ignore_error), for processes
  7. # where LogStruct doesn't set up ActionMailer (development, the console and Rake tasks by default).
  8. # Without them those mailers raised NameError there. Each logs the error with the mailer's logger,
  9. # then ignores it, reports it to Rails.error, or raises it again, as its name says.
  10. 2 module FallbackErrorHandlers
  11. 2 extend T::Sig
  12. 2 extend T::Helpers
  13. 2 requires_ancestor { ::ActionMailer::Base }
  14. 2 class << self
  15. 2 extend T::Sig
  16. 3 sig { params(logger: T.untyped, error: StandardError, report: T::Boolean, reraise: T::Boolean).void }
  17. 2 def handle(logger, error, report:, reraise:)
  18. 3 logger&.error("#{error.class}: #{error.message}")
  19. 3 ::Rails.error.report(error, handled: true, source: "logstruct.action_mailer") if report
  20. 3 Kernel.raise error if reraise
  21. end
  22. end
  23. # The handlers, for a mailer and (through ClassMethods) for the mailer class, which
  24. # ActionMailer::MailDeliveryJob passes an error to. Both have a logger.
  25. 2 module Handlers
  26. 2 extend T::Sig
  27. 2 extend T::Helpers
  28. 2 abstract!
  29. 3 sig { params(ex: StandardError).void }
  30. 2 def log_and_ignore_error(ex)
  31. 1 FallbackErrorHandlers.handle(handler_logger, ex, report: false, reraise: false)
  32. end
  33. 3 sig { params(ex: StandardError).void }
  34. 2 def log_and_report_error(ex)
  35. 1 FallbackErrorHandlers.handle(handler_logger, ex, report: true, reraise: false)
  36. end
  37. 3 sig { params(ex: StandardError).void }
  38. 2 def log_and_reraise_error(ex)
  39. 1 FallbackErrorHandlers.handle(handler_logger, ex, report: false, reraise: true)
  40. end
  41. 2 private
  42. 3 sig { abstract.returns(T.untyped) }
  43. 2 def handler_logger
  44. end
  45. end
  46. 2 include Handlers
  47. 2 module ClassMethods
  48. 2 extend T::Sig
  49. 2 extend T::Helpers
  50. 2 requires_ancestor { T.class_of(::ActionMailer::Base) }
  51. 2 include Handlers
  52. 2 private
  53. 3 sig { override.returns(T.untyped) }
  54. 2 def handler_logger = logger
  55. end
  56. 2 private
  57. 3 sig { override.returns(T.untyped) }
  58. 2 def handler_logger = logger
  59. end
  60. end
  61. end
  62. end

lib/log_struct/integrations/action_mailer/metadata_collection.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module ActionMailer
  6. # Handles collection of metadata for email logging
  7. 2 module MetadataCollection
  8. 2 extend T::Sig
  9. # Add message-specific metadata to log data
  10. 3 sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
  11. 2 def self.add_message_metadata(mailer, log_data)
  12. 93 message = mailer.respond_to?(:message) ? mailer.message : nil
  13. # Add attachment count if message is available
  14. 93 log_data[:attachment_count] = if message
  15. 92 message.attachments&.count || 0
  16. else
  17. 1 0
  18. end
  19. end
  20. # Add context metadata to log data
  21. 3 sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
  22. 2 def self.add_context_metadata(mailer, log_data)
  23. # Add account ID information if available (but not user email)
  24. 91 extract_ids_to_log_data(mailer, log_data)
  25. end
  26. 3 sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
  27. 2 def self.extract_ids_to_log_data(mailer, log_data)
  28. # Use configured ID mapping from LogStruct configuration
  29. 91 id_mapping = LogStruct.config.integrations.actionmailer_id_mapping
  30. 91 id_mapping.each do |ivar_name, log_key|
  31. 184 ivar = :"@#{ivar_name}"
  32. 184 next unless mailer.instance_variable_defined?(ivar)
  33. 4 obj = mailer.instance_variable_get(ivar)
  34. 4 log_data[log_key] = obj.id if obj.respond_to?(:id)
  35. end
  36. end
  37. end
  38. end
  39. end
  40. end

lib/log_struct/integrations/active_job.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "active_job"
  5. 2 require "active_job/log_subscriber"
  6. rescue LoadError
  7. # ActiveJob gem is not available, integration will be skipped
  8. end
  9. 2 require_relative "active_job/log_subscriber" if defined?(::ActiveJob::LogSubscriber)
  10. 2 module LogStruct
  11. 2 module Integrations
  12. # ActiveJob integration for structured logging
  13. 2 module ActiveJob
  14. 2 extend T::Sig
  15. 2 extend IntegrationInterface
  16. # Set up ActiveJob structured logging
  17. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  18. 2 def self.setup(config)
  19. 5 return nil unless defined?(::ActiveJob::LogSubscriber)
  20. 5 return nil unless config.enabled
  21. 5 return nil unless config.integrations.enable_activejob
  22. 4 ::ActiveSupport.on_load(:active_job) do
  23. # Detach the default text formatter
  24. 4 RailsLogSubscribers.detach(::ActiveJob::LogSubscriber, :active_job)
  25. # Attach our structured formatter
  26. 4 Integrations::ActiveJob::LogSubscriber.attach_to :active_job
  27. end
  28. 4 true
  29. end
  30. end
  31. end
  32. end

lib/log_struct/integrations/active_job/log_subscriber.rb

100.0% lines covered

126 relevant lines. 126 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/log_subscriber"
  4. 2 require_relative "../../enums/source"
  5. 2 require_relative "../../enums/event"
  6. 2 require_relative "../../log/active_job"
  7. 2 require_relative "../../log/error"
  8. 2 require_relative "../event_time"
  9. 2 module LogStruct
  10. 2 module Integrations
  11. 2 module ActiveJob
  12. # Structured logging for ActiveJob
  13. 2 class LogSubscriber < ::ActiveSupport::LogSubscriber
  14. 2 extend T::Sig
  15. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  16. 2 def enqueue(event)
  17. 5 log_enqueue(event)
  18. end
  19. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  20. 2 def enqueue_at(event)
  21. 1 log_enqueue(event)
  22. end
  23. # perform_all_later: a line for each job, as perform_later writes
  24. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  25. 2 def enqueue_all(event)
  26. 9 ts = EventTime.coerce_event_time(event.time)
  27. 9 jobs = Array(event.payload[:jobs]).grep(::ActiveJob::Base)
  28. 9 error = event.payload[:exception_object]
  29. 9 if error
  30. 4 log_enqueue_all_error(error, jobs, ts)
  31. 4 return
  32. end
  33. # Adapters with their own enqueue_all (Sidekiq's) say only how many jobs they enqueued
  34. 14 if jobs.none? { |job| job.successfully_enqueued? || job.enqueue_error }
  35. 4 log_enqueued_count(jobs, event.payload[:enqueued_count], ts)
  36. 4 return
  37. end
  38. 1 jobs.each do |job|
  39. 3 if job.successfully_enqueued?
  40. 2 log_enqueued(job, ts)
  41. 1 elsif job.enqueue_error
  42. 1 log_exception(job.enqueue_error, job)
  43. end
  44. end
  45. end
  46. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  47. 2 def perform(event)
  48. 22 job = T.cast(event.payload[:job], ::ActiveJob::Base)
  49. 22 exception = event.payload[:exception_object]
  50. 22 if exception
  51. # Log the exception with the job context
  52. 6 log_exception(exception, job)
  53. # An aborted job didn't run: a before_perform callback stopped it
  54. 16 elsif !event.payload[:aborted]
  55. 15 finished_at = EventTime.coerce_event_time(event.end)
  56. 15 base_fields = build_base_fields(job)
  57. 15 logger.info(Log::ActiveJob::Finish.new(
  58. **base_fields.to_kwargs,
  59. duration_ms: event.duration.to_f,
  60. finished_at: finished_at,
  61. timestamp: finished_at
  62. ))
  63. end
  64. end
  65. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  66. 2 def perform_start(event)
  67. 14 job = T.cast(event.payload[:job], ::ActiveJob::Base)
  68. 14 ts = EventTime.coerce_event_time(event.time)
  69. 14 started_at = ts
  70. 14 attempt = job.executions
  71. 14 base_fields = build_base_fields(job)
  72. 14 logger.info(Log::ActiveJob::Start.new(
  73. **base_fields.to_kwargs,
  74. started_at: started_at,
  75. attempt: attempt,
  76. timestamp: ts
  77. ))
  78. end
  79. # retry_on caught the exception and enqueued the job again
  80. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  81. 2 def enqueue_retry(event)
  82. 1 job = T.cast(event.payload[:job], ::ActiveJob::Base)
  83. # retry_job can be called without an exception or a wait
  84. 1 error = event.payload[:error]
  85. 1 wait = event.payload[:wait]
  86. 1 logger.info(Log::ActiveJob::Retry.new(
  87. **build_base_fields(job).to_kwargs,
  88. error_class: error&.class,
  89. message: error&.message,
  90. 1 wait_ms: wait.nil? ? nil : wait.to_f * 1000,
  91. timestamp: EventTime.coerce_event_time(event.time)
  92. ))
  93. end
  94. # retry_on ran out of attempts. The job then raises the exception (an error line), unless
  95. # retry_on was given a block.
  96. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  97. 2 def retry_stopped(event)
  98. 1 job = T.cast(event.payload[:job], ::ActiveJob::Base)
  99. 1 error = T.cast(event.payload[:error], Exception)
  100. 1 logger.error(Log::ActiveJob::RetryStopped.new(
  101. **build_base_fields(job).to_kwargs,
  102. error_class: error.class,
  103. message: error.message,
  104. timestamp: EventTime.coerce_event_time(event.time)
  105. ))
  106. end
  107. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  108. 2 def discard(event)
  109. 1 job = T.cast(event.payload[:job], ::ActiveJob::Base)
  110. 1 error = T.cast(event.payload[:error], Exception)
  111. 1 logger.error(Log::ActiveJob::Discard.new(
  112. **build_base_fields(job).to_kwargs,
  113. error_class: error.class,
  114. message: error.message,
  115. timestamp: EventTime.coerce_event_time(event.time)
  116. ))
  117. end
  118. 2 private
  119. # Like Rails' own enqueue lines: a job the queue didn't take writes an error line, and one a
  120. # before_enqueue callback stopped writes none
  121. 3 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  122. 2 def log_enqueue(event)
  123. 6 job = T.cast(event.payload[:job], ::ActiveJob::Base)
  124. # perform_later rescues ActiveJob::EnqueueError and keeps it on the job
  125. 6 error = event.payload[:exception_object] || job.enqueue_error
  126. 6 if error
  127. # With the inline adapter, a job that fails raises out of perform_later, and its perform line has
  128. # already logged the error
  129. 3 log_exception(error, job) unless LogStruct::LoggedErrors.logged?(error)
  130. 3 elsif !event.payload[:aborted]
  131. 2 log_enqueued(job, EventTime.coerce_event_time(event.time))
  132. end
  133. end
  134. 3 sig { params(job: ::ActiveJob::Base, ts: Time).void }
  135. 2 def log_enqueued(job, ts)
  136. 12 base_fields = build_base_fields(job)
  137. 12 scheduled_at = job.scheduled_at
  138. 12 if scheduled_at
  139. 1 logger.info(Log::ActiveJob::Schedule.new(**base_fields.to_kwargs, scheduled_at: scheduled_at, timestamp: ts))
  140. else
  141. 11 logger.info(Log::ActiveJob::Enqueue.new(**base_fields.to_kwargs, timestamp: ts))
  142. end
  143. end
  144. # perform_all_later raised: the adapter did (Redis down, say), or with the inline adapter a job did
  145. 3 sig { params(error: Exception, jobs: T::Array[::ActiveJob::Base], ts: Time).void }
  146. 2 def log_enqueue_all_error(error, jobs, ts)
  147. # Rails' own enqueue loop (for adapters without enqueue_all) marks each job it tries, keeps an
  148. # ActiveJob::EnqueueError on its job and goes on, and stops at a job that raises anything else
  149. 15 tried = jobs.reject { |job| job.successfully_enqueued?.nil? }
  150. 4 tried.each do |job|
  151. 7 enqueue_error = job.enqueue_error
  152. 7 if job.successfully_enqueued?
  153. 4 log_enqueued(job, ts)
  154. 3 elsif enqueue_error
  155. 1 log_exception(enqueue_error, job)
  156. end
  157. end
  158. # With the inline adapter, the error is a job's own, which its perform line has logged
  159. 4 return if LogStruct::LoggedErrors.logged?(error)
  160. 4 raised_by = tried.last
  161. 4 if raised_by && !raised_by.successfully_enqueued? && raised_by.enqueue_error.nil?
  162. 2 log_exception(error, raised_by)
  163. 2 return
  164. end
  165. # Which job it was isn't known: an adapter's own enqueue_all (Sidekiq's) doesn't say which jobs it
  166. # enqueued before it raised
  167. 6 classes = jobs.map { |job| job.class.to_s }.uniq.join(", ")
  168. 2 message = "perform_all_later of #{jobs.size} jobs (#{classes}) failed"
  169. 2 message += "; the queue adapter doesn't say which it enqueued" if tried.empty?
  170. 2 logger.error(Log::Error.new(
  171. source: Source::Job,
  172. error_class: error.class,
  173. message: message,
  174. error_message: error.message,
  175. backtrace: error.backtrace,
  176. level: Level::Error,
  177. timestamp: ts
  178. ))
  179. 2 LogStruct::LoggedErrors.mark(error)
  180. end
  181. # Every job's line when the adapter enqueued them all; when it didn't, it doesn't say which it enqueued
  182. 3 sig { params(jobs: T::Array[::ActiveJob::Base], enqueued_count: T.untyped, ts: Time).void }
  183. 2 def log_enqueued_count(jobs, enqueued_count, ts)
  184. 4 return if jobs.empty?
  185. 4 if enqueued_count == jobs.size
  186. 6 jobs.each { |job| log_enqueued(job, ts) }
  187. else
  188. 6 classes = jobs.map { |job| job.class.to_s }.uniq.join(", ")
  189. 2 logger.warn("perform_all_later enqueued #{enqueued_count.to_i} of #{jobs.size} jobs (#{classes}); the queue adapter doesn't say which")
  190. end
  191. end
  192. 3 sig { params(job: ::ActiveJob::Base).returns(Log::ActiveJob::BaseFields) }
  193. 2 def build_base_fields(job)
  194. 56 Log::ActiveJob::BaseFields.new(
  195. job_id: job.job_id,
  196. job_class: job.class.to_s,
  197. queue_name: job.queue_name&.to_sym,
  198. executions: job.executions,
  199. # Backends use Strings or Integers (SolidQueue uses its job row ID)
  200. provider_job_id: job.provider_job_id&.to_s,
  201. arguments: logged_arguments(job)
  202. )
  203. end
  204. # ActionMailer's delivery job carries the mail's own arguments, which can be secrets: Devise passes its
  205. # confirmation, password reset and invitation tokens as plain strings, which no key-based filter can find.
  206. # For a mail, log the mailer, the mail and the delivery method only.
  207. 3 sig { params(job: ::ActiveJob::Base).returns(T.nilable(T::Array[T.untyped])) }
  208. 2 def logged_arguments(job)
  209. 56 return nil unless job.class.respond_to?(:log_arguments?) && job.class.log_arguments?
  210. 54 return job.arguments.first(3) if defined?(::ActionMailer::MailDeliveryJob) && job.is_a?(::ActionMailer::MailDeliveryJob)
  211. 29 job.arguments
  212. end
  213. # perform_now lets any Exception through, not only StandardErrors
  214. 3 sig { params(exception: Exception, job: ::ActiveJob::Base).void }
  215. 2 def log_exception(exception, job)
  216. 12 log_data = Log::ActiveJob::Error.new(
  217. **build_base_fields(job).to_kwargs,
  218. error_class: exception.class,
  219. message: exception.message,
  220. backtrace: exception.backtrace
  221. )
  222. 12 logger.error(log_data)
  223. # So a backend that reports the same exception (GoodJob's on_thread_error) doesn't log it again
  224. 12 LogStruct::LoggedErrors.mark(exception)
  225. end
  226. 3 sig { returns(T.untyped) }
  227. 2 def logger
  228. 147 ::ActiveJob::Base.logger
  229. end
  230. end
  231. end
  232. end
  233. end

lib/log_struct/integrations/active_model_serializers.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/notifications"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. # ActiveModelSerializers integration. Subscribes to AMS notifications and
  7. # emits structured logs with serializer/adapter/duration details.
  8. 2 module ActiveModelSerializers
  9. 2 extend T::Sig
  10. 4 sig { params(config: LogStruct::Configuration).returns(T.nilable(TrueClass)) }
  11. 2 def self.setup(config)
  12. 4 return nil unless defined?(::ActiveSupport::Notifications)
  13. # Only activate if AMS appears to be present
  14. 4 return nil unless defined?(::ActiveModelSerializers)
  15. # AMS logs each render as a line of text too
  16. 3 if defined?(::ActiveModelSerializers::Logging::LogSubscriber)
  17. 1 ::ActiveModelSerializers::Logging::LogSubscriber.detach_from(:active_model_serializers)
  18. end
  19. # Subscribe to common AMS notification names; keep broad but specific
  20. 3 pattern = /\.active_model_serializers\z/
  21. 3 ::ActiveSupport::Notifications.subscribe(pattern) do |_name, started, finished, _unique_id, payload|
  22. # started/finished are Time; convert to ms
  23. 2 duration_ms = ((finished - started) * 1000.0).round(3)
  24. 2 serializer = payload[:serializer] || payload[:serializer_class]
  25. 2 adapter = payload[:adapter]
  26. 2 resource = payload[:resource] || payload[:object]
  27. 2 LogStruct.info(
  28. LogStruct::Log::ActiveModelSerializers.new(
  29. message: "ams.render",
  30. serializer: serializer&.name,
  31. adapter: adapter&.class&.name,
  32. resource_class: resource&.class&.name,
  33. duration_ms: duration_ms,
  34. timestamp: started
  35. )
  36. )
  37. rescue => e
  38. # A problem with the log line is LogStruct's own, and never fails the render in production
  39. 1 LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :active_model_serializers})
  40. end
  41. 3 true
  42. end
  43. end
  44. end
  45. end

lib/log_struct/integrations/active_record.rb

93.48% lines covered

138 relevant lines. 129 lines covered and 9 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/notifications"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. # ActiveRecord Integration for SQL Query Logging
  7. #
  8. # This integration captures and structures all SQL queries executed through ActiveRecord,
  9. # providing detailed performance and debugging information in a structured format.
  10. #
  11. # ## Features:
  12. # - Captures all SQL queries with execution time
  13. # - Safely filters sensitive data from bind parameters
  14. # - Extracts database operation metadata
  15. # - Provides connection pool monitoring information
  16. # - Identifies query types and table names
  17. #
  18. # ## Performance Considerations:
  19. # - Minimal overhead on query execution
  20. # - Async logging prevents I/O blocking
  21. # - Configurable to disable in production if needed
  22. # - Smart filtering reduces log volume for repetitive queries
  23. #
  24. # ## Security:
  25. # - Bind parameters are filtered by their column names, like request params, and values that
  26. # look like tokens or hashes are filtered too
  27. # - The SQL is scrubbed like any string (emails, card numbers), but it isn't filtered: it can
  28. # hold literal values (string conditions, or every value when prepared_statements is off)
  29. #
  30. # ## Configuration:
  31. # ```ruby
  32. # LogStruct.configure do |config|
  33. # config.integrations.enable_sql_logging = true
  34. # config.integrations.sql_slow_query_threshold = 100.0 # ms
  35. # config.integrations.sql_log_bind_params = false # disable in production
  36. # end
  37. # ```
  38. 2 module ActiveRecord
  39. 2 extend T::Sig
  40. 2 extend IntegrationInterface
  41. # Track subscription state keyed to the current Notifications.notifier instance
  42. 2 State = ::Struct.new(:subscribed, :notifier_id)
  43. 2 STATE = T.let(State.new(false, nil), State)
  44. # Set up SQL query logging integration
  45. 3 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  46. 2 def self.setup(config)
  47. 21 return nil unless config.integrations.enable_sql_logging
  48. 20 return nil unless defined?(::ActiveRecord::Base)
  49. # Detach Rails' default ActiveRecord log subscriber to prevent
  50. # duplicate/unstructured SQL debug output when LogStruct SQL logging
  51. # is enabled. We still receive notifications via ActiveSupport.
  52. 19 if defined?(::ActiveRecord::LogSubscriber)
  53. begin
  54. RailsLogSubscribers.detach(::ActiveRecord::LogSubscriber, :active_record)
  55. rescue => e
  56. LogStruct.handle_exception(e, source: LogStruct::Source::Internal)
  57. end
  58. end
  59. # Disable verbose query logs ("↳ caller") since LogStruct provides
  60. # structured context and these lines are noisy/unstructured.
  61. # (A setting on the ActiveRecord module, so ActiveRecord::Base isn't loaded early.)
  62. 19 ::ActiveRecord.verbose_query_logs = false
  63. 19 subscribe_to_sql_notifications
  64. 19 true
  65. end
  66. 2 private_class_method
  67. # Subscribe to ActiveRecord's sql.active_record notifications
  68. 3 sig { void }
  69. 2 def self.subscribe_to_sql_notifications
  70. # Avoid duplicate subscriptions; re-subscribe if the notifier was reset
  71. 19 notifier = ::ActiveSupport::Notifications.notifier
  72. 19 current_id = notifier&.object_id
  73. 19 if STATE.subscribed && STATE.notifier_id == current_id
  74. return
  75. end
  76. 19 ::ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
  77. 9 handle_sql_event(name, start, finish, id, payload)
  78. rescue => error
  79. 1 LogStruct.handle_exception(error, source: LogStruct::Source::Internal)
  80. end
  81. 19 STATE.subscribed = true
  82. 19 STATE.notifier_id = current_id
  83. end
  84. # Process SQL notification event and create structured log
  85. 3 sig { params(name: String, start: T.untyped, finish: T.untyped, id: String, payload: T::Hash[Symbol, T.untyped]).void }
  86. 2 def self.handle_sql_event(name, start, finish, id, payload)
  87. # Skip schema queries and Rails internal queries
  88. 38 return if skip_query?(payload)
  89. 31 duration_ms = ((finish - start) * 1000.0).round(2)
  90. # Skip fast queries if threshold is configured
  91. 31 config = LogStruct.config
  92. 31 if config.integrations.sql_slow_query_threshold&.positive?
  93. 4 return if duration_ms < config.integrations.sql_slow_query_threshold
  94. end
  95. 29 sql_log = Log::SQL.new(
  96. message: format_sql_message(payload),
  97. source: Source::App,
  98. event: Event::Database,
  99. sql: payload[:sql]&.strip || "",
  100. name: payload[:name] || "SQL Query",
  101. duration_ms: duration_ms,
  102. row_count: extract_row_count(payload),
  103. adapter: extract_adapter_name(payload),
  104. bind_params: extract_and_filter_binds(payload),
  105. database_name: extract_database_name(payload),
  106. connection_pool_size: extract_pool_size(payload),
  107. active_connections: extract_active_connections(payload),
  108. operation_type: extract_operation_type(payload),
  109. table_names: extract_table_names(payload)
  110. )
  111. 28 LogStruct.info(sql_log)
  112. end
  113. # Determine if query should be skipped from logging
  114. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
  115. 2 def self.skip_query?(payload)
  116. 38 query_name = payload[:name]
  117. 38 sql = payload[:sql]
  118. # Skip Rails schema queries
  119. 38 return true if query_name&.include?("SCHEMA")
  120. 37 return true if query_name&.include?("CACHE")
  121. # Skip common Rails internal queries
  122. 36 return true if sql&.include?("schema_migrations")
  123. 35 return true if sql&.include?("ar_internal_metadata")
  124. # Skip SHOW/DESCRIBE queries
  125. 34 return true if sql&.match?(/\A\s*(SHOW|DESCRIBE|EXPLAIN)\s/i)
  126. 31 false
  127. end
  128. # Format a readable message for the SQL log
  129. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(String) }
  130. 2 def self.format_sql_message(payload)
  131. 29 operation_name = payload[:name] || "SQL Query"
  132. 29 "#{operation_name} executed"
  133. end
  134. # Extract row count from payload
  135. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
  136. 2 def self.extract_row_count(payload)
  137. 29 row_count = payload[:row_count]
  138. 29 row_count.is_a?(Integer) ? row_count : nil
  139. end
  140. # Extract database adapter name
  141. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
  142. 2 def self.extract_adapter_name(payload)
  143. 29 connection = payload[:connection]
  144. 29 return nil unless connection
  145. 27 adapter_name = connection.class.name
  146. 27 adapter_name&.split("::")&.last
  147. end
  148. # Extract and filter bind parameters
  149. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[T.untyped])) }
  150. 2 def self.extract_and_filter_binds(payload)
  151. 29 return nil unless LogStruct.config.integrations.sql_log_bind_params
  152. 27 binds = payload[:binds]
  153. # The values as the database gets them, which Rails passes as a lambda since 6.0
  154. 27 casted_binds = payload[:type_casted_binds]
  155. 27 casted_binds = casted_binds.call if casted_binds.respond_to?(:call)
  156. 27 return nil unless binds.is_a?(Array) || casted_binds.is_a?(Array)
  157. 7 attributes = binds.is_a?(Array) ? binds : []
  158. 7 values = casted_binds.is_a?(Array) ? casted_binds : attributes
  159. 7 values.each_with_index.map do |value, index|
  160. 19 filter_bind(attributes[index], value)
  161. end
  162. end
  163. # A bind's value, filtered by its column name like Rails' own SQL log lines
  164. # (ActiveRecord::LogSubscriber#render_bind)
  165. 3 sig { params(attribute: T.untyped, value: T.untyped).returns(T.untyped) }
  166. 2 def self.filter_bind(attribute, value)
  167. 21 name = if attribute.is_a?(Array)
  168. attribute.first&.name
  169. 21 elsif attribute.respond_to?(:name)
  170. 8 attribute.name
  171. end
  172. 21 value = attribute.value_for_database if value.equal?(attribute) && attribute.respond_to?(:value_for_database)
  173. 21 if attribute.respond_to?(:type) && attribute.type.respond_to?(:binary?) && attribute.type.binary? && !value.nil?
  174. 2 return "<#{value.to_s.bytesize} bytes of binary data>"
  175. end
  176. 19 return "[FILTERED]" if name && ParamFilters.should_filter_key?(name.to_s, value)
  177. 16 filter_bind_parameter(value)
  178. end
  179. # Extract database name from connection
  180. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
  181. 2 def self.extract_database_name(payload)
  182. 29 connection = payload[:connection]
  183. 29 return nil unless connection
  184. 27 if connection.respond_to?(:current_database)
  185. 27 connection.current_database
  186. elsif connection.respond_to?(:database)
  187. connection.database
  188. end
  189. rescue
  190. nil
  191. end
  192. # Extract connection pool size
  193. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
  194. 2 def self.extract_pool_size(payload)
  195. 29 connection = payload[:connection]
  196. 29 return nil unless connection
  197. 27 pool = connection.pool if connection.respond_to?(:pool)
  198. 27 pool&.size
  199. rescue
  200. nil
  201. end
  202. # Extract active connection count
  203. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
  204. 2 def self.extract_active_connections(payload)
  205. 29 connection = payload[:connection]
  206. 29 return nil unless connection
  207. 27 pool = connection.pool if connection.respond_to?(:pool)
  208. 27 pool&.stat&.[](:busy)
  209. rescue
  210. nil
  211. end
  212. # Extract SQL operation type (SELECT, INSERT, etc.)
  213. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
  214. 2 def self.extract_operation_type(payload)
  215. 29 sql = payload[:sql]
  216. 29 return nil unless sql
  217. # Extract first word of SQL query
  218. 29 match = sql.strip.match(/\A\s*(\w+)/i)
  219. 29 match&.captures&.first&.upcase
  220. end
  221. # Extract table names from SQL query
  222. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[String])) }
  223. 2 def self.extract_table_names(payload)
  224. 29 sql = payload[:sql]
  225. 29 return nil unless sql
  226. # Simple regex to extract table names (basic implementation)
  227. # This covers most common cases but could be enhanced
  228. 29 tables = []
  229. # Match FROM, JOIN, UPDATE, INSERT INTO, DELETE FROM patterns
  230. 29 sql.scan(/(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM)\s+["`]?(\w+)["`]?/i) do |match|
  231. 29 table_name = match[0]
  232. 29 tables << table_name unless tables.include?(table_name)
  233. end
  234. 29 tables.empty? ? nil : tables
  235. end
  236. # Filter individual bind parameter values to remove sensitive data
  237. 3 sig { params(value: T.untyped).returns(T.untyped) }
  238. 2 def self.filter_bind_parameter(value)
  239. 26 case value
  240. when String
  241. # Filter strings that look like passwords, tokens, secrets, etc.
  242. 23 if looks_sensitive?(value)
  243. 8 "[FILTERED]"
  244. else
  245. 15 value
  246. end
  247. else
  248. 3 value
  249. end
  250. end
  251. # Whether a String value is shaped like a secret: a token, a digest or a Bearer header. Values are
  252. # filtered by their column names first; this catches binds without one. Text that mentions a
  253. # password or a key isn't a secret.
  254. 3 sig { params(value: String).returns(T::Boolean) }
  255. 2 def self.looks_sensitive?(value)
  256. # A long value with no spaces, which might be a token
  257. 23 return true if value.length > 50 && !value.match?(/\s/)
  258. # Filter strings that look like hashed passwords, API keys, tokens
  259. 21 return true if value.match?(/\A[a-f0-9]{32,}\z/i) # MD5, SHA, etc.
  260. 19 return true if value.match?(/\A[A-Za-z0-9+\/]{20,}={0,2}\z/) # Base64
  261. # Filter JWT tokens (header.payload.signature format, starts with "ey")
  262. 18 return true if value.match?(/\Aey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\z/)
  263. # Filter Bearer tokens
  264. 17 return true if value.match?(/\ABearer\s+/i)
  265. 15 false
  266. end
  267. end
  268. end
  269. end

lib/log_struct/integrations/active_storage.rb

45.1% lines covered

51 relevant lines. 23 lines covered and 28 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../enums/source"
  4. 2 require_relative "../enums/event"
  5. 2 require_relative "../log/active_storage"
  6. 2 module LogStruct
  7. 2 module Integrations
  8. # Integration for ActiveStorage structured logging
  9. 2 module ActiveStorage
  10. 2 extend T::Sig
  11. 2 extend IntegrationInterface
  12. # Set up ActiveStorage structured logging
  13. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  14. 2 def self.setup(config)
  15. 2 return nil unless defined?(::ActiveStorage)
  16. 1 return nil unless config.enabled
  17. 1 return nil unless config.integrations.enable_activestorage
  18. 1 detach_rails_log_subscriber
  19. # Subscribe to all ActiveStorage service events
  20. 1 ::ActiveSupport::Notifications.subscribe(/service_.*\.active_storage/) do |*args|
  21. process_active_storage_event(::ActiveSupport::Notifications::Event.new(*args), config)
  22. rescue => e
  23. # A problem with a line mustn't fail the upload or download it describes
  24. LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :active_storage})
  25. end
  26. 1 true
  27. end
  28. # LogStruct's lines replace Rails' own ActiveStorage lines. Rails loads its log subscriber with
  29. # the storage service, after LogStruct is set up, so it's loaded here to detach it.
  30. 3 sig { void }
  31. 2 def self.detach_rails_log_subscriber
  32. 1 require "active_storage/log_subscriber"
  33. 1 RailsLogSubscribers.detach(::ActiveStorage::LogSubscriber, :active_storage)
  34. rescue LoadError
  35. nil
  36. end
  37. 2 private_class_method
  38. # Process ActiveStorage events and create structured logs
  39. 2 sig { params(event: ActiveSupport::Notifications::Event, config: LogStruct::Configuration).void }
  40. 2 def self.process_active_storage_event(event, config)
  41. return unless config.enabled
  42. return unless config.integrations.enable_activestorage
  43. # Extract key information from the event
  44. event_name = event.name.sub(/\.active_storage$/, "")
  45. service_name = event.payload[:service]
  46. duration_ms = event.duration
  47. # Map service events to log event types
  48. event_type = case event_name
  49. when "service_upload"
  50. Event::Upload
  51. when "service_download"
  52. Event::Download
  53. when "service_delete"
  54. Event::Delete
  55. when "service_delete_prefixed"
  56. Event::Delete
  57. when "service_exist"
  58. Event::Exist
  59. when "service_url"
  60. Event::Url
  61. when "service_download_chunk", "service_streaming_download"
  62. Event::Download
  63. when "service_update_metadata"
  64. Event::Metadata
  65. else
  66. # Events with no line of their own, such as service_mirror
  67. return
  68. end
  69. # Create structured log event using generated classes
  70. log_data = case event_type
  71. when Event::Upload
  72. Log::ActiveStorage::Upload.new(
  73. storage: service_name.to_sym,
  74. file_id: event.payload[:key]&.to_s,
  75. checksum: event.payload[:checksum]&.to_s,
  76. duration_ms: duration_ms
  77. )
  78. when Event::Download
  79. Log::ActiveStorage::Download.new(
  80. storage: service_name.to_sym,
  81. file_id: event.payload[:key]&.to_s,
  82. range: event.payload[:range],
  83. duration_ms: duration_ms
  84. )
  85. when Event::Delete
  86. # delete_prefixed has a prefix instead of a key
  87. Log::ActiveStorage::Delete.new(
  88. storage: service_name.to_sym,
  89. file_id: event.payload[:key]&.to_s,
  90. prefix: event.payload[:prefix]&.to_s
  91. )
  92. when Event::Metadata
  93. Log::ActiveStorage::Metadata.new(
  94. storage: service_name.to_sym,
  95. file_id: event.payload[:key]&.to_s,
  96. mime_type: event.payload[:content_type]&.to_s
  97. )
  98. when Event::Exist
  99. Log::ActiveStorage::Exist.new(
  100. storage: service_name.to_sym,
  101. file_id: event.payload[:key]&.to_s,
  102. exist: event.payload[:exist]
  103. )
  104. when Event::Url
  105. # Without the URL: it's signed, and anyone who has it can read the file until it expires
  106. Log::ActiveStorage::Url.new(
  107. storage: service_name.to_sym,
  108. file_id: event.payload[:key]&.to_s
  109. )
  110. end
  111. # Rails logs existence checks and URLs at debug level: an app generates a URL for each file it shows
  112. if event_type == Event::Exist || event_type == Event::Url
  113. LogStruct.debug(log_data)
  114. else
  115. LogStruct.info(log_data)
  116. end
  117. end
  118. end
  119. end
  120. end

lib/log_struct/integrations/ahoy.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. # Ahoy analytics integration. If Ahoy is present, prepend a small hook to
  6. # Ahoy::Tracker#track to emit a structured log for analytics events.
  7. 2 module Ahoy
  8. 2 extend T::Sig
  9. 4 sig { params(config: LogStruct::Configuration).returns(T.nilable(TrueClass)) }
  10. 2 def self.setup(config)
  11. 6 return nil unless defined?(::Ahoy)
  12. 4 if defined?(::Ahoy::Tracker)
  13. 4 mod = Module.new do
  14. 4 extend T::Sig
  15. # Takes whatever Ahoy's track takes and passes it on as given, so Ahoy's defaults still apply
  16. 8 sig { params(name: T.untyped, args: T.untyped).returns(T.untyped) }
  17. 4 def track(name, *args)
  18. 4 result = super
  19. begin
  20. 4 LogStruct.info(
  21. LogStruct::Log::Ahoy.new(
  22. message: "ahoy.track",
  23. ahoy_event: name.to_s,
  24. properties: Integrations::Ahoy.properties_for_log(args.first)
  25. )
  26. )
  27. rescue => e
  28. # A problem with the log line is LogStruct's, and never fails the app's tracking call in production
  29. 1 LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :ahoy})
  30. end
  31. 4 result
  32. end
  33. end
  34. 4 ::Ahoy::Tracker.prepend(mod)
  35. end
  36. 4 true
  37. end
  38. # Event properties can come from the browser, so they can be anything; a non-Hash is logged as its class
  39. 3 sig { params(properties: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
  40. 2 def self.properties_for_log(properties)
  41. 4 properties = properties.to_unsafe_h if properties.respond_to?(:to_unsafe_h)
  42. 4 case properties
  43. 2 when nil then nil
  44. 3 when Hash then properties.to_h { |key, value| [key.to_s.to_sym, value] }
  45. 1 else {_class: properties.class.name}
  46. end
  47. end
  48. end
  49. end
  50. end

lib/log_struct/integrations/carrierwave.rb

100.0% lines covered

47 relevant lines. 47 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "carrierwave"
  5. rescue LoadError
  6. # CarrierWave gem is not available, integration will be skipped
  7. end
  8. 2 module LogStruct
  9. 2 module Integrations
  10. # CarrierWave integration for structured logging
  11. 2 module CarrierWave
  12. 2 extend T::Sig
  13. 2 extend IntegrationInterface
  14. # Set up CarrierWave structured logging
  15. 3 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  16. 2 def self.setup(config)
  17. 8 return nil unless defined?(::CarrierWave)
  18. 8 return nil unless config.enabled
  19. 8 return nil unless config.integrations.enable_carrierwave
  20. # Patch CarrierWave to add logging
  21. 8 ::CarrierWave::Uploader::Base.prepend(LoggingMethods)
  22. 8 true
  23. end
  24. # Logs the operations that store, delete or fetch a file: store! (an upload, when there's a
  25. # cached file to store), remove! (a delete) and download! (a remote URL fetched into the cache).
  26. # retrieve_from_store! isn't logged: it only points the uploader at a stored file, and runs
  27. # each time a record's mounted file is read.
  28. 2 module LoggingMethods
  29. 2 extend T::Sig
  30. 2 extend T::Helpers
  31. 2 requires_ancestor { ::CarrierWave::Uploader::Base }
  32. 3 sig { params(args: T.untyped).returns(T.untyped) }
  33. 2 def store!(*args)
  34. # CarrierWave only stores a cached file; a record's save calls store! on every uploader
  35. 6 storing = !cache_only && (cached? || !args.first.nil?)
  36. 6 start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  37. 6 result = super
  38. 6 return result unless storing && !cached?
  39. 5 duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0
  40. 5 log_file_operation do
  41. # file_fields is a Hash Sorbet can't check against the struct's keyword arguments
  42. 5 Log::CarrierWave::Upload.new(
  43. **T.unsafe(file_fields),
  44. filename: file&.filename,
  45. mime_type: file&.content_type,
  46. size: file&.size,
  47. duration_ms: duration_ms
  48. )
  49. end
  50. 5 result
  51. end
  52. 3 sig { returns(T.untyped) }
  53. 2 def remove!
  54. 2 fields = file ? file_fields : nil
  55. 2 result = super
  56. 3 log_file_operation { Log::CarrierWave::Delete.new(**T.unsafe(fields)) } if fields
  57. 2 result
  58. end
  59. 3 sig { params(uri: T.untyped, remote_headers: T.untyped).returns(T.untyped) }
  60. 2 def download!(uri, remote_headers = {})
  61. 1 result = super
  62. 1 log_file_operation do
  63. 1 Log::CarrierWave::Download.new(
  64. **T.unsafe(file_fields),
  65. filename: file&.filename,
  66. mime_type: file&.content_type,
  67. size: file&.size
  68. )
  69. end
  70. 1 result
  71. end
  72. 2 private
  73. # The fields every CarrierWave line has
  74. 3 sig { returns(T::Hash[Symbol, T.untyped]) }
  75. 2 def file_fields
  76. {
  77. 7 storage: storage.class.name.to_s.split("::").last.to_s.downcase.to_sym,
  78. file_id: identifier.to_s,
  79. uploader: self.class.name,
  80. model: model&.class&.name,
  81. mount_point: mounted_as&.to_s,
  82. version: version_name&.to_s,
  83. store_path: store_path,
  84. extension: file&.extension
  85. }
  86. end
  87. # A problem with a log line mustn't stop the file operation that has already happened
  88. 3 sig { params(block: T.proc.returns(Log::Interfaces::CommonFields)).void }
  89. 2 def log_file_operation(&block)
  90. 7 LogStruct.info(yield)
  91. rescue => e
  92. 1 LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :carrierwave})
  93. end
  94. end
  95. end
  96. end
  97. end

lib/log_struct/integrations/dotenv.rb

74.83% lines covered

151 relevant lines. 113 lines covered and 38 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # rubocop:disable Sorbet/ConstantsFromStrings
  4. 2 require_relative "../boot_buffer"
  5. 2 require "pathname"
  6. begin
  7. 2 require "dotenv-rails"
  8. rescue LoadError
  9. # Dotenv-rails gem is not available, integration will be skipped
  10. end
  11. 2 module LogStruct
  12. 2 module Integrations
  13. # Dotenv integration: emits structured logs for load/update/save/restore events
  14. 2 module Dotenv
  15. 2 extend T::Sig
  16. 2 extend IntegrationInterface
  17. 2 @original_logger_setter = T.let(nil, T.nilable(UnboundMethod))
  18. # Internal state holder to avoid duplicate subscriptions in a Sorbet-friendly way
  19. 2 State = ::Struct.new(:subscribed)
  20. 2 STATE = T.let(State.new(false), State)
  21. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  22. 2 def self.setup(config)
  23. # Subscribe regardless of dotenv gem presence so instrumentation via
  24. # ActiveSupport::Notifications can be captured during tests and runtime.
  25. 2 subscribe!
  26. 2 true
  27. end
  28. 2 class << self
  29. 2 extend T::Sig
  30. 4 sig { params(env: T.untyped).returns(String) }
  31. 2 def relative_env_path(env)
  32. 4 abs = env.filename
  33. begin
  34. 1 if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
  35. 1 Pathname.new(abs).relative_path_from(Pathname.new(::Rails.root.to_s)).to_s
  36. else
  37. abs
  38. end
  39. rescue
  40. abs
  41. end
  42. end
  43. # A problem with a Dotenv log line is LogStruct's own: raised in development and test, logged in production
  44. 3 sig { params(error: StandardError).void }
  45. 2 def handle_log_error(error)
  46. 3 LogStruct.handle_exception(error, source: LogStruct::Source::Internal, context: {integration: :dotenv})
  47. end
  48. 4 sig { void }
  49. 2 def subscribe!
  50. # Guard against double subscription
  51. 5 return if STATE.subscribed
  52. 3 instrumenter = defined?(::ActiveSupport::Notifications) ? ::ActiveSupport::Notifications : nil
  53. 3 return unless instrumenter
  54. 3 instrumenter.subscribe("load.dotenv") do |*args|
  55. # Allow tests to stub Log::Dotenv.new to force an error path
  56. 2 LogStruct::Log::Dotenv.new
  57. 2 event = ::ActiveSupport::Notifications::Event.new(*args)
  58. 2 env = event.payload[:env]
  59. 2 file = relative_env_path(env)
  60. ts = event.time ? Time.at(event.time) : Time.now
  61. LogStruct.info(Log::Dotenv::Load.new(file: file, timestamp: ts))
  62. rescue => e
  63. 2 handle_log_error(e)
  64. end
  65. 3 instrumenter.subscribe("update.dotenv") do |*args|
  66. LogStruct::Log::Dotenv.new
  67. event = ::ActiveSupport::Notifications::Event.new(*args)
  68. diff = event.payload[:diff]
  69. vars = diff.env.keys.map(&:to_s)
  70. ts = event.time ? Time.at(event.time) : Time.now
  71. LogStruct.debug(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
  72. rescue => e
  73. handle_log_error(e)
  74. end
  75. 3 instrumenter.subscribe("save.dotenv") do |*args|
  76. 1 LogStruct::Log::Dotenv.new
  77. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  78. 1 ts = event.time ? Time.at(event.time) : Time.now
  79. 1 LogStruct.info(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
  80. rescue => e
  81. handle_log_error(e)
  82. end
  83. 3 instrumenter.subscribe("restore.dotenv") do |*args|
  84. LogStruct::Log::Dotenv.new
  85. event = ::ActiveSupport::Notifications::Event.new(*args)
  86. diff = event.payload[:diff]
  87. vars = diff.env.keys.map(&:to_s)
  88. ts = event.time ? Time.at(event.time) : Time.now
  89. LogStruct.info(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
  90. rescue => e
  91. handle_log_error(e)
  92. end
  93. 3 STATE.subscribed = true
  94. end
  95. end
  96. # Early boot subscription to buffer structured logs until logger is ready
  97. 2 @@boot_subscribed = T.let(false, T::Boolean)
  98. 4 sig { void }
  99. 2 def self.setup_boot
  100. 2 return if @@boot_subscribed
  101. 2 return unless defined?(::ActiveSupport::Notifications)
  102. 2 instrumenter = if Object.const_defined?(:Dotenv)
  103. 2 dm = Object.const_get(:Dotenv)
  104. 2 dm.respond_to?(:instrumenter) ? dm.instrumenter : ::ActiveSupport::Notifications
  105. else
  106. ::ActiveSupport::Notifications
  107. end
  108. 2 instrumenter.subscribe("load.dotenv") do |*args|
  109. 2 event = ::ActiveSupport::Notifications::Event.new(*args)
  110. 2 env = event.payload[:env]
  111. 2 file = relative_env_path(env)
  112. 1 ts = event.time ? Time.at(event.time) : Time.now
  113. 1 LogStruct::BootBuffer.add(Log::Dotenv::Load.new(file: file, timestamp: ts))
  114. rescue => e
  115. 1 handle_log_error(e)
  116. end
  117. 2 instrumenter.subscribe("update.dotenv") do |*args|
  118. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  119. 1 diff = event.payload[:diff]
  120. 1 vars = diff.env.keys.map(&:to_s)
  121. 1 ts = event.time ? Time.at(event.time) : Time.now
  122. 1 LogStruct::BootBuffer.add(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
  123. rescue => e
  124. handle_log_error(e)
  125. end
  126. 2 instrumenter.subscribe("save.dotenv") do |*args|
  127. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  128. 1 ts = event.time ? Time.at(event.time) : Time.now
  129. 1 LogStruct::BootBuffer.add(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
  130. rescue => e
  131. handle_log_error(e)
  132. end
  133. 2 instrumenter.subscribe("restore.dotenv") do |*args|
  134. event = ::ActiveSupport::Notifications::Event.new(*args)
  135. diff = event.payload[:diff]
  136. vars = diff.env.keys.map(&:to_s)
  137. ts = event.time ? Time.at(event.time) : Time.now
  138. LogStruct::BootBuffer.add(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
  139. rescue => e
  140. handle_log_error(e)
  141. end
  142. 2 @@boot_subscribed = true
  143. end
  144. # Intercept Dotenv::Rails#logger= to defer replay until we resolve policy
  145. 4 sig { void }
  146. 2 def self.intercept_logger_setter!
  147. 2 return unless Object.const_defined?(:Dotenv)
  148. # Do not intercept when LogStruct is disabled; allow original dotenv replay
  149. 2 return unless LogStruct.enabled?
  150. 2 dotenv_mod = Object.const_get(:Dotenv)
  151. 2 return unless dotenv_mod.const_defined?(:Rails)
  152. 2 klass = dotenv_mod.const_get(:Rails)
  153. 2 return if klass.instance_variable_defined?(:@_logstruct_replay_patched)
  154. 2 original = klass.instance_method(:logger=)
  155. 2 @original_logger_setter = original
  156. 2 mod = Module.new do
  157. 2 define_method :logger= do |new_logger|
  158. # Defer replay: store desired logger, keep ReplayLogger as current
  159. 2 instance_variable_set(:@logstruct_pending_dotenv_logger, new_logger)
  160. 2 new_logger
  161. end
  162. 2 define_method :logstruct_pending_dotenv_logger do
  163. 2 instance_variable_get(:@logstruct_pending_dotenv_logger)
  164. end
  165. end
  166. 2 klass.prepend(mod)
  167. 2 klass.instance_variable_set(:@_logstruct_replay_patched, true)
  168. end
  169. # Decide which boot logs to emit after user initializers
  170. 4 sig { void }
  171. 2 def self.resolve_boot_logs!
  172. # If LogStruct is disabled, do not alter dotenv behavior at all
  173. 2 return unless LogStruct.enabled?
  174. 2 dotenv_mod = Object.const_defined?(:Dotenv) ? Object.const_get(:Dotenv) : nil
  175. 2 klass = dotenv_mod&.const_defined?(:Rails) ? dotenv_mod.const_get(:Rails) : nil
  176. 2 pending_logger = nil
  177. 2 railtie_instance = nil
  178. 2 if klass&.respond_to?(:instance)
  179. 2 railtie_instance = klass.instance
  180. 2 if railtie_instance.respond_to?(:logstruct_pending_dotenv_logger)
  181. 2 pending_logger = railtie_instance.logstruct_pending_dotenv_logger
  182. end
  183. end
  184. 2 if LogStruct.enabled? && LogStruct.config.integrations.enable_dotenv
  185. # Structured path
  186. 2 if pending_logger && railtie_instance
  187. # Clear any buffered original logs
  188. 2 current_logger = railtie_instance.logger if railtie_instance.respond_to?(:logger)
  189. 2 if current_logger && current_logger.class.name.end_with?("ReplayLogger")
  190. begin
  191. 2 logs = current_logger.instance_variable_get(:@logs)
  192. 2 logs.clear if logs.respond_to?(:clear)
  193. rescue
  194. # best effort
  195. end
  196. end
  197. 2 railtie_instance.config.dotenv.logger = pending_logger
  198. end
  199. # Detach original subscriber and subscribe runtime structured
  200. 2 if dotenv_mod&.const_defined?(:LogSubscriber)
  201. 2 dotenv_mod.const_get(:LogSubscriber).detach_from(:dotenv)
  202. end
  203. 2 LogStruct::Integrations::Dotenv.subscribe!
  204. 2 require_relative "../boot_buffer"
  205. 2 LogStruct::BootBuffer.flush
  206. else
  207. # Original path: replay dotenv lines, drop structured buffer
  208. if railtie_instance && @original_logger_setter
  209. setter = @original_logger_setter
  210. new_logger = pending_logger
  211. if new_logger.nil? && ENV["RAILS_LOG_TO_STDOUT"].to_s.strip != ""
  212. require "logger"
  213. require "active_support/tagged_logging"
  214. new_logger = ActiveSupport::TaggedLogging.new(::Logger.new($stdout)).tagged("dotenv")
  215. end
  216. setter.bind_call(railtie_instance, new_logger) if new_logger
  217. end
  218. require_relative "../boot_buffer"
  219. LogStruct::BootBuffer.clear
  220. end
  221. end
  222. end
  223. end
  224. end
  225. # Subscribe immediately to capture earliest dotenv events into BootBuffer
  226. 2 LogStruct::Integrations::Dotenv.setup_boot
  227. # rubocop:enable Sorbet/ConstantsFromStrings

lib/log_struct/integrations/event_time.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module EventTime
  6. 2 extend T::Sig
  7. 3 sig { params(value: T.untyped).returns(Time) }
  8. 2 def self.coerce_event_time(value)
  9. 43 return value if value.is_a?(Time)
  10. 43 return Time.now unless value.is_a?(Numeric)
  11. 43 monotonic_now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
  12. 43 Time.now - (monotonic_now - value)
  13. end
  14. end
  15. end
  16. end

lib/log_struct/integrations/good_job.rb

100.0% lines covered

70 relevant lines. 70 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "good_job"
  5. rescue LoadError
  6. # GoodJob gem is not available, integration will be skipped
  7. end
  8. 2 require_relative "good_job/logger" if defined?(::GoodJob)
  9. 2 module LogStruct
  10. 2 module Integrations
  11. # GoodJob integration for structured logging
  12. #
  13. # GoodJob is a PostgreSQL-based ActiveJob backend. Its jobs are logged by the ActiveJob
  14. # integration (enqueued, started, finished, failed), so this integration covers GoodJob itself:
  15. #
  16. # - Replaces GoodJob.logger with a LogStruct logger, so GoodJob's own messages are JSON lines
  17. # - Logs each thread error as a structured error line, then calls the app's `on_thread_error`
  18. # - Stops GoodJob's own log subscriber logging those errors again with the whole backtrace
  19. #
  20. # ## Configuration:
  21. # The integration is automatically enabled when GoodJob is detected and
  22. # LogStruct configuration allows it. It can be disabled by setting:
  23. #
  24. # ```ruby
  25. # config.integrations.enable_goodjob = false
  26. # ```
  27. 2 module GoodJob
  28. 2 extend T::Sig
  29. 2 extend IntegrationInterface
  30. # Set up GoodJob structured logging
  31. #
  32. # This method replaces GoodJob's logger and logs its thread errors as structured lines.
  33. #
  34. # @param config [LogStruct::Configuration] The LogStruct configuration
  35. # @return [Boolean, nil] Returns true if setup was successful, nil if skipped
  36. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  37. 2 def self.setup(config)
  38. 12 return nil unless defined?(::GoodJob)
  39. 6 return nil unless config.enabled
  40. 6 return nil unless config.integrations.enable_goodjob
  41. # Replace GoodJob's logger with our structured logger
  42. 6 configure_logger
  43. 6 compact_error_lines
  44. 6 true
  45. end
  46. # GoodJob::LogSubscriber logs errors as text with the whole backtrace in the message (over 10KB).
  47. # GoodJob passes every error its job tasks raise, every systemd watchdog error and every notifier
  48. # error except connection errors to `on_thread_error`, which LogThreadErrors logs as a structured
  49. # line, so GoodJob's own lines for those are off. The notifier passes connection errors on only
  50. # once they repeat, so its line for them is kept, without the backtrace.
  51. 2 module CompactErrorLines
  52. 2 extend T::Sig
  53. 2 extend T::Helpers
  54. 2 requires_ancestor { ::GoodJob::LogSubscriber }
  55. 3 sig { params(_event: T.untyped).void }
  56. 2 def finished_job_task(_event)
  57. end
  58. 3 sig { params(event: T.untyped).void }
  59. 2 def notifier_notify_error(event)
  60. 2 exception = event.payload[:error]
  61. 2 return unless exception
  62. 2 return unless LogStruct::Integrations::GoodJob.notifier_connection_error?(exception)
  63. 2 error { "Notifier errored: #{exception.class}: #{exception}" }
  64. end
  65. 3 sig { params(_event: T.untyped).void }
  66. 2 def systemd_watchdog_error(_event)
  67. end
  68. end
  69. # Logs each thread error as a structured line, then calls the handler the app set as
  70. # `on_thread_error` (GoodJob's README sets one to report errors to an error tracker). Every
  71. # GoodJob component passes its thread errors to `GoodJob._on_thread_error`, so this works
  72. # whenever the app sets its handler: GoodJob's engine applies config.good_job.on_thread_error
  73. # after LogStruct's setup. GoodJob passes any Exception its handled_exceptions setting lets
  74. # through, not only StandardErrors.
  75. 2 module LogThreadErrors
  76. 2 extend T::Sig
  77. 3 sig { params(exception: Exception).void }
  78. 2 def _on_thread_error(exception)
  79. 6 LogStruct::Integrations::GoodJob.log_thread_error(exception)
  80. rescue => e
  81. 1 LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :good_job})
  82. ensure
  83. # The app's handler runs even if LogStruct's line failed
  84. 6 super
  85. end
  86. end
  87. # Configure GoodJob to use LogStruct's structured logger
  88. 3 sig { void }
  89. 2 def self.configure_logger
  90. 10 return unless defined?(::GoodJob)
  91. # Not GoodJob, which here is this module
  92. 10 goodjob_module = ::GoodJob
  93. # Replace GoodJob.logger with our structured logger if GoodJob is available
  94. 10 if goodjob_module.respond_to?(:logger=)
  95. 10 goodjob_module.logger = LogStruct::Integrations::GoodJob::Logger.new("GoodJob")
  96. # GoodJob's executable adds a plain text STDOUT logger unless GoodJob.logger already writes
  97. # to STDOUT, which it can't tell for this logger
  98. 10 if defined?(::GoodJob::CLI) && ::GoodJob::CLI.respond_to?(:log_to_stdout=)
  99. 10 ::GoodJob::CLI.log_to_stdout = false
  100. end
  101. end
  102. 10 configure_thread_error_handler
  103. end
  104. 3 sig { void }
  105. 2 def self.configure_thread_error_handler
  106. 10 return unless defined?(::GoodJob)
  107. # Not GoodJob, which here is this module
  108. 10 goodjob_singleton = ::GoodJob.singleton_class
  109. 10 return unless goodjob_singleton.method_defined?(:_on_thread_error)
  110. 10 goodjob_singleton.prepend(LogThreadErrors) unless goodjob_singleton.ancestors.include?(LogThreadErrors)
  111. end
  112. # A failed job's exception was already logged by the ActiveJob integration, with the job's
  113. # ID and class, so it isn't logged again here
  114. 3 sig { params(exception: Exception).void }
  115. 2 def self.log_thread_error(exception)
  116. 5 return if LogStruct::LoggedErrors.logged?(exception)
  117. 4 ::GoodJob.logger.error(
  118. LogStruct::Log::GoodJob::Error.new(
  119. error_class: exception.class.name || exception.class.to_s,
  120. error_message: exception.message,
  121. backtrace: exception.backtrace,
  122. level: LogStruct::Level::Error,
  123. process_id: ::Process.pid,
  124. thread_id: Thread.current.object_id.to_s(36)
  125. )
  126. )
  127. end
  128. # The errors GoodJob's notifier holds back until they repeat (GoodJob::Notifier::CONNECTION_ERRORS)
  129. 3 sig { params(exception: Exception).returns(T::Boolean) }
  130. 2 def self.notifier_connection_error?(exception)
  131. 2 return true unless defined?(::GoodJob::Notifier::CONNECTION_ERRORS)
  132. 2 ::GoodJob::Notifier::CONNECTION_ERRORS.any? do |error_name|
  133. 2 error_class = error_name.safe_constantize
  134. 2 error_class.is_a?(Class) && exception.is_a?(error_class)
  135. end
  136. end
  137. # Only when LogThreadErrors logs thread errors instead
  138. 3 sig { void }
  139. 2 def self.compact_error_lines
  140. 6 return unless defined?(::GoodJob::LogSubscriber)
  141. 2 return unless ::GoodJob.singleton_class.ancestors.include?(LogThreadErrors)
  142. 2 log_subscriber = ::GoodJob::LogSubscriber
  143. 2 log_subscriber.prepend(CompactErrorLines) unless log_subscriber.ancestors.include?(CompactErrorLines)
  144. end
  145. 2 private_class_method :configure_logger
  146. 2 private_class_method :compact_error_lines
  147. 2 private_class_method :configure_thread_error_handler
  148. end
  149. end
  150. end

lib/log_struct/integrations/good_job/logger.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 require_relative "../../semantic_logger/logger"
  4. 1 require_relative "../../log/good_job"
  5. 1 require_relative "../../enums/source"
  6. 1 module LogStruct
  7. 1 module Integrations
  8. 1 module GoodJob
  9. # Custom Logger for GoodJob that creates LogStruct::Log::GoodJob entries
  10. #
  11. # GoodJob's own messages are logged as Log::GoodJob::Log lines with the process and thread,
  12. # and, during a job, the job's ID, class, queue, execution count, scheduled time and priority.
  13. #
  14. # ## Usage:
  15. # This logger is automatically configured when the GoodJob integration
  16. # is enabled. It replaces GoodJob.logger to provide structured logging
  17. # for all GoodJob operations.
  18. 1 class Logger < LogStruct::SemanticLogger::Logger
  19. 1 extend T::Sig
  20. # Override log methods to create GoodJob-specific log structs
  21. 1 %i[debug info warn error fatal].each do |level|
  22. 5 define_method(level) do |message = nil, payload = nil, exception = nil, &block|
  23. # LogStruct log entries and Hashes (like the thread error handler's) are logged as they are, and a
  24. # message with an exception is an error line with both
  25. 18 if !(message.nil? || message.is_a?(String)) || exception || payload.is_a?(Exception)
  26. 5 next super(message, payload, exception, &block)
  27. end
  28. 13 job_context = Logger.current_job_context
  29. 13 log_struct = Log::GoodJob::Log.new(
  30. 1 message: message || (block ? block.call : ""),
  31. process_id: ::Process.pid,
  32. thread_id: Thread.current.object_id.to_s(36),
  33. job_id: job_context[:job_id],
  34. job_class: job_context[:job_class],
  35. queue_name: job_context[:queue_name],
  36. executions: job_context[:executions],
  37. scheduled_at: job_context[:scheduled_at],
  38. priority: job_context[:priority]
  39. )
  40. 13 super(log_struct, payload, &nil)
  41. end
  42. end
  43. # The job GoodJob is running on this thread: CurrentThread.job (a GoodJob::Job) in GoodJob 4,
  44. # CurrentThread.execution in GoodJob 3
  45. 2 sig { returns(T::Hash[Symbol, T.untyped]) }
  46. 1 def self.current_job_context
  47. 13 return {} unless defined?(::GoodJob::CurrentThread)
  48. 3 current = ::GoodJob::CurrentThread
  49. 3 job = (current.job if current.respond_to?(:job)) || (current.execution if current.respond_to?(:execution))
  50. 3 return {} unless job
  51. 2 job_class = job.try(:job_class) || job.try(:serialized_params)&.dig("job_class")
  52. {
  53. 2 job_id: job.try(:active_job_id)&.to_s,
  54. job_class: job_class&.to_s,
  55. queue_name: job.try(:queue_name)&.to_sym,
  56. executions: job.try(:executions_count) || job.try(:executions),
  57. scheduled_at: job.try(:scheduled_at),
  58. priority: job.try(:priority)
  59. }
  60. end
  61. end
  62. end
  63. end
  64. end

lib/log_struct/integrations/host_authorization.rb

97.78% lines covered

45 relevant lines. 44 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "action_dispatch/middleware/host_authorization"
  4. 2 require_relative "../enums/event"
  5. 2 require_relative "../log/security/blocked_host"
  6. 2 module LogStruct
  7. 2 module Integrations
  8. # Host Authorization integration for structured logging of blocked hosts
  9. 2 module HostAuthorization
  10. 2 extend T::Sig
  11. 2 extend IntegrationInterface
  12. 2 RESPONSE_HTML = T.let(
  13. "<html><head><title>Blocked Host</title></head><body>" \
  14. "<h1>Blocked Host</h1>" \
  15. "<p>This host is not permitted to access this application.</p>" \
  16. "<p>If you are the administrator, check your configuration.</p>" \
  17. "</body></html>",
  18. String
  19. )
  20. 2 RESPONSE_HEADERS = T.let(
  21. {
  22. "Content-Type" => "text/html",
  23. "Content-Length" => RESPONSE_HTML.bytesize.to_s
  24. }.freeze,
  25. T::Hash[String, String]
  26. )
  27. 2 FORBIDDEN_STATUS = T.let(403, Integer)
  28. # Set up host authorization logging
  29. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  30. 2 def self.setup(config)
  31. 7 return nil unless config.enabled
  32. 7 return nil unless config.integrations.enable_host_authorization
  33. # Merge our response_app into existing host_authorization config to preserve excludes
  34. 7 existing = Rails.application.config.host_authorization
  35. 7 unless existing.is_a?(Hash)
  36. existing = {}
  37. end
  38. 7 existing = existing.dup
  39. 7 app_response_app = existing[:response_app]
  40. # Set up once: don't wrap our own response app again
  41. 7 app_response_app = app_response_app.app_response_app if app_response_app.is_a?(ResponseApp)
  42. 7 existing[:response_app] = ResponseApp.new(app_response_app)
  43. 7 Rails.application.config.host_authorization = existing
  44. 7 true
  45. end
  46. # Logs the blocked host, then responds with the app's own response_app if it set one
  47. 2 class ResponseApp
  48. 2 extend T::Sig
  49. 3 sig { returns(T.untyped) }
  50. 2 attr_reader :app_response_app
  51. 4 sig { params(app_response_app: T.untyped).void }
  52. 2 def initialize(app_response_app)
  53. 7 @app_response_app = app_response_app
  54. end
  55. 3 sig { params(env: T::Hash[String, T.untyped]).returns(T.untyped) }
  56. 2 def call(env)
  57. begin
  58. 4 log_blocked_host(env)
  59. rescue => e
  60. 1 LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :host_authorization})
  61. end
  62. 4 return @app_response_app.call(env) if @app_response_app
  63. # Dup the headers so they can be modified by downstream middleware
  64. 3 [FORBIDDEN_STATUS, RESPONSE_HEADERS.dup, [RESPONSE_HTML]]
  65. end
  66. 2 private
  67. 3 sig { params(env: T::Hash[String, T.untyped]).void }
  68. 2 def log_blocked_host(env)
  69. 4 request = ::ActionDispatch::Request.new(env)
  70. # The hosts Rails blocked: the Host header, and X-Forwarded-Host when it's set
  71. 4 blocked_hosts = Array(env["action_dispatch.blocked_hosts"]).map(&:to_s)
  72. 4 log_entry = LogStruct::Log::Security::BlockedHost.new(
  73. message: "Blocked host detected: #{request.host}",
  74. blocked_host: request.host,
  75. 4 blocked_hosts: blocked_hosts.empty? ? nil : blocked_hosts,
  76. path: request.path,
  77. http_method: request.method,
  78. source_ip: request.ip,
  79. user_agent: request.user_agent,
  80. referer: StringScrubber.without_query(request.referer),
  81. x_forwarded_for: request.x_forwarded_for
  82. )
  83. 4 LogStruct.warn(log_entry)
  84. end
  85. end
  86. end
  87. end
  88. end

lib/log_struct/integrations/integration_interface.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. # Interface that all integrations must implement
  6. # This ensures consistent behavior across all integration modules
  7. 2 module IntegrationInterface
  8. 2 extend T::Sig
  9. 2 extend T::Helpers
  10. # This is an interface that should be implemented by all integration modules
  11. 2 interface!
  12. # All integrations must implement this method to set up their functionality
  13. # @return [Boolean, nil] Returns true if setup was successful, nil if skipped
  14. 4 sig { abstract.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  15. 2 def setup(config); end
  16. end
  17. end
  18. end

lib/log_struct/integrations/lograge.rb

97.22% lines covered

108 relevant lines. 105 lines covered and 3 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "lograge"
  5. rescue LoadError
  6. # Lograge gem is not available, integration will be skipped
  7. end
  8. 2 module LogStruct
  9. 2 module Integrations
  10. # Lograge integration for structured request logging
  11. 2 module Lograge
  12. 2 extend IntegrationInterface
  13. 2 LOGRAGE_KNOWN_KEYS = T.let(
  14. [
  15. :method,
  16. :path,
  17. :format,
  18. :controller,
  19. :action,
  20. :status,
  21. :duration,
  22. :view,
  23. :db,
  24. :params,
  25. :request_id,
  26. :source_ip,
  27. :user_agent,
  28. :referer,
  29. :host,
  30. :content_type,
  31. :accept
  32. ].freeze,
  33. T::Array[Symbol]
  34. )
  35. # With config.exceptions_app = routes, Rails renders an error page by sending the request through the app again
  36. # as GET /<status>, and keeps the request's own method and path in these headers
  37. 2 ORIGINAL_METHOD_HEADER = "action_dispatch.original_request_method"
  38. 2 ORIGINAL_PATH_HEADER = "action_dispatch.original_path"
  39. # Set when the request's action logged a line with its error, so the error page for it logs none
  40. 2 ACTION_ERROR_LOGGED_HEADER = "logstruct.action_error_logged"
  41. # A Proc, not a Method: sorbet-runtime replaces a method with sig checks on its first call, so a Method taken
  42. # before then isn't equal to one taken after, and setup couldn't tell it had added the test already
  43. 2 IGNORE_REPEATED_ERROR_PAGE = T.let(
  44. ->(event) { Lograge.repeated_error_page?(event) },
  45. T.proc.params(event: ActiveSupport::Notifications::Event).returns(T::Boolean)
  46. )
  47. 2 class << self
  48. 2 extend T::Sig
  49. # Set up lograge for structured request logging
  50. 4 sig { override.params(logstruct_config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  51. 2 def setup(logstruct_config)
  52. 3 return nil unless defined?(::Lograge)
  53. 3 return nil unless logstruct_config.enabled
  54. 3 return nil unless logstruct_config.integrations.enable_lograge
  55. 3 configure_lograge(logstruct_config)
  56. 3 true
  57. end
  58. 2 private_class_method
  59. 4 sig { params(logstruct_config: LogStruct::Configuration).void }
  60. 2 def configure_lograge(logstruct_config)
  61. 3 ::Rails.application.configure do
  62. 3 config.lograge.enabled = true
  63. # Use a raw formatter that just returns the log struct.
  64. # The struct is converted to JSON by our Formatter (after filtering, etc.)
  65. 3 config.lograge.formatter = T.let(
  66. lambda do |data|
  67. 4 LogStruct::Integrations::Lograge.build_request_log(data)
  68. end,
  69. T.proc.params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request)
  70. )
  71. # Add custom options to lograge
  72. 3 config.lograge.custom_options = lambda do |event|
  73. Integrations::Lograge.lograge_default_options(event)
  74. end
  75. # Lograge detaches Rails' own request lines from ActiveSupport::Notifications. From Rails 8.2 they come
  76. # from Rails.event, so LogStruct detaches them there.
  77. 3 config.after_initialize do |app|
  78. 3 Integrations::Lograge.unsubscribe_rails_request_lines unless app.config.lograge.keep_original_rails_log
  79. end
  80. end
  81. # Lograge.setup adds the app's own ignore tests to the same list
  82. 3 ::Lograge.ignore(IGNORE_REPEATED_ERROR_PAGE) unless ::Lograge.ignore_tests.include?(IGNORE_REPEATED_ERROR_PAGE)
  83. end
  84. # Rails' "Processing by", "Completed 200 OK" and "Rendered" lines, which the request line replaces
  85. 4 sig { void }
  86. 2 def unsubscribe_rails_request_lines
  87. # Earlier Rails versions' subscribers are lograge's to detach
  88. 3 return unless defined?(::ActiveSupport::EventReporter::LogSubscriber)
  89. 1 RailsLogSubscribers.unsubscribe_from_events(::ActionController::LogSubscriber) if defined?(::ActionController::LogSubscriber)
  90. 1 RailsLogSubscribers.unsubscribe_from_events(::ActionView::LogSubscriber) if defined?(::ActionView::LogSubscriber)
  91. end
  92. 3 sig { params(event: ActiveSupport::Notifications::Event).returns(T::Hash[Symbol, T.untyped]) }
  93. 2 def lograge_default_options(event)
  94. # Extract essential fields from the payload
  95. 12 options = event.payload.slice(
  96. :request_id,
  97. :host,
  98. :source_ip
  99. ).compact
  100. 12 add_request_address(event.payload[:request], options)
  101. 12 add_error_page_request(event, options)
  102. 12 if event.payload[:params].present?
  103. options[:params] = event.payload[:params].except("controller", "action")
  104. end
  105. # Process headers if available
  106. 12 process_headers(event, options)
  107. # Apply custom options from application if provided
  108. 12 apply_custom_options(event, options)
  109. 12 options
  110. end
  111. # Rails' payload has the request but not its address, which Rails' own "Started GET" line logs.
  112. # A payload's own :source_ip and :host (from append_info_to_payload) take precedence.
  113. 3 sig { params(request: T.untyped, options: T::Hash[Symbol, T.untyped]).void }
  114. 2 def add_request_address(request, options)
  115. 12 return unless request.is_a?(::ActionDispatch::Request)
  116. 6 options[:host] ||= request.host
  117. 6 options[:source_ip] ||= begin
  118. 5 request.remote_ip
  119. rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError
  120. # The forwarding headers disagree, so the address the connection came from
  121. 1 request.get_header("REMOTE_ADDR")
  122. end
  123. end
  124. # An error page's line has the method and path of the request it's for, not GET /500
  125. 3 sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
  126. 2 def add_error_page_request(event, options)
  127. 12 request = event.payload[:request]
  128. 12 return unless request.is_a?(::ActionDispatch::Request)
  129. 6 if request.has_header?(ORIGINAL_PATH_HEADER)
  130. 1 options[:method] = request.get_header(ORIGINAL_METHOD_HEADER)
  131. 1 options[:path] = "#{request.script_name}#{request.get_header(ORIGINAL_PATH_HEADER)}"
  132. 5 elsif event.payload[:exception]
  133. 1 request.set_header(ACTION_ERROR_LOGGED_HEADER, true)
  134. end
  135. end
  136. # The error page for an error its action's line logged would log the request a second time. A request whose
  137. # error came before any action (a routing error) or after it (in a middleware) keeps its error page's line.
  138. 3 sig { params(event: ActiveSupport::Notifications::Event).returns(T::Boolean) }
  139. 2 def repeated_error_page?(event)
  140. 4 request = event.payload[:request]
  141. 4 request.is_a?(::ActionDispatch::Request) && request.has_header?(ORIGINAL_PATH_HEADER) &&
  142. request.get_header(ACTION_ERROR_LOGGED_HEADER) == true
  143. end
  144. # Process headers from the event payload
  145. 3 sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
  146. 2 def process_headers(event, options)
  147. 12 headers = event.payload[:headers]
  148. 12 return if headers.blank?
  149. # Rails' ActionDispatch::RequestId middleware stores request_id in headers
  150. # Only set if not already present in payload (payload takes precedence)
  151. 3 if options[:request_id].blank? && headers["action_dispatch.request_id"].present?
  152. 1 options[:request_id] = headers["action_dispatch.request_id"]
  153. end
  154. 3 options[:user_agent] = headers["HTTP_USER_AGENT"]
  155. 3 options[:referer] = headers["HTTP_REFERER"]
  156. 3 options[:content_type] = headers["CONTENT_TYPE"]
  157. 3 options[:accept] = headers["HTTP_ACCEPT"]
  158. end
  159. # Apply custom options from the application's configuration
  160. 3 sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
  161. 2 def apply_custom_options(event, options)
  162. 12 custom_options_proc = LogStruct.config.integrations.lograge_custom_options
  163. 12 return unless custom_options_proc&.respond_to?(:call)
  164. # The proc can change options, or return a Hash of fields to add, as Lograge's custom_options do
  165. 3 result = custom_options_proc.call(event, options)
  166. 3 return if !result.is_a?(Hash) || result.equal?(options)
  167. 6 options.merge!(result.to_h { |key, value| [key.to_s.to_sym, value] })
  168. end
  169. 3 sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request) }
  170. 2 def build_request_log(data)
  171. 6 normalized_data = normalize_lograge_data(data)
  172. # Coerce common fields to expected types
  173. 6 status = ((s = normalized_data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
  174. 6 duration_ms = ((d = normalized_data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
  175. 6 view = ((v = normalized_data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
  176. 6 db = ((b = normalized_data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
  177. 6 params = normalized_data[:params]
  178. 6 params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
  179. 6 additional_data = extract_additional_data(normalized_data)
  180. 6 Log::Request.new(
  181. http_method: normalized_data[:method]&.to_s,
  182. path: normalized_data[:path]&.to_s,
  183. format: normalized_data[:format]&.to_sym,
  184. controller: normalized_data[:controller]&.to_s,
  185. action: normalized_data[:action]&.to_s,
  186. status: status,
  187. duration_ms: duration_ms,
  188. view: view,
  189. database: db,
  190. params: params,
  191. source_ip: normalized_data[:source_ip]&.to_s,
  192. user_agent: normalized_data[:user_agent]&.to_s,
  193. # Without its query, which can carry reset tokens and OAuth codes
  194. referer: StringScrubber.without_query(normalized_data[:referer]&.to_s),
  195. host: normalized_data[:host]&.to_s,
  196. content_type: normalized_data[:content_type]&.to_s,
  197. accept: normalized_data[:accept]&.to_s,
  198. additional_data: additional_data,
  199. timestamp: Time.now
  200. )
  201. end
  202. 3 sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
  203. 2 def normalize_lograge_data(data)
  204. 6 data.each_with_object({}) do |(key, value), normalized|
  205. 47 normalized[key.to_s.to_sym] = value
  206. end
  207. end
  208. 3 sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
  209. 2 def extract_additional_data(data)
  210. 6 extras = T.let({}, T::Hash[Symbol, T.untyped])
  211. 6 data.each do |key, value|
  212. 47 next if LOGRAGE_KNOWN_KEYS.include?(key)
  213. 8 next if value.nil?
  214. 6 extras[key] = value
  215. end
  216. 6 return nil if extras.empty?
  217. 3 extras
  218. end
  219. end
  220. end
  221. end
  222. end

lib/log_struct/integrations/puma.rb

61.45% lines covered

262 relevant lines. 161 lines covered and 101 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module Puma
  6. 2 extend T::Sig
  7. 2 extend T::Helpers
  8. 2 STATE = T.let(
  9. {
  10. installed: false,
  11. boot_emitted: false,
  12. shutdown_emitted: false,
  13. handler_pending_started: false,
  14. signals_trapped: false,
  15. start_info: {
  16. mode: nil,
  17. puma_version: nil,
  18. puma_codename: nil,
  19. ruby_version: nil,
  20. min_threads: nil,
  21. max_threads: nil,
  22. environment: nil,
  23. pid: nil,
  24. listening: []
  25. }
  26. },
  27. T::Hash[Symbol, T.untyped]
  28. )
  29. 2 class << self
  30. 2 extend T::Sig
  31. 4 sig { params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  32. 2 def setup(config)
  33. 2 return nil unless config.integrations.enable_puma
  34. # No stdout wrapping here.
  35. # Ensure Puma is loaded so we can patch its classes
  36. begin
  37. 2 require "puma"
  38. rescue LoadError
  39. # If Puma isn't available, skip setup
  40. 1 return nil
  41. end
  42. 1 install_patches!
  43. 1 if ARGV.include?("server")
  44. # Emit deterministic boot/started events based on CLI args
  45. begin
  46. port = port_from_argv(ARGV)
  47. si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
  48. si[:pid] ||= Process.pid
  49. si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
  50. si[:mode] ||= "single"
  51. if port && !T.cast(si[:listening], T::Array[T.untyped]).any? { |a| a.to_s.include?(":" + port.to_s) }
  52. si[:listening] = ["tcp://127.0.0.1:#{port}"]
  53. end
  54. emit_boot_if_needed!
  55. unless STATE[:started_emitted]
  56. emit_started!
  57. STATE[:started_emitted] = true
  58. end
  59. rescue => e
  60. handle_integration_error(e)
  61. end
  62. begin
  63. unless STATE[:signals_trapped]
  64. STATE[:signals_trapped] = true
  65. %w[TERM INT].each { |signal| trap_shutdown_signal(signal) }
  66. end
  67. rescue => e
  68. handle_integration_error(e)
  69. end
  70. at_exit do
  71. emit_shutdown!("Exiting")
  72. rescue => e
  73. handle_integration_error(e)
  74. end
  75. # Connection-based readiness: emit started once port is accepting connections
  76. # No background threads or sockets; rely solely on parsing Puma output
  77. end
  78. 1 true
  79. end
  80. 4 sig { void }
  81. 2 def install_patches!
  82. 2 return if STATE[:installed]
  83. 2 STATE[:installed] = true
  84. 2 state_reset!
  85. begin
  86. begin
  87. 2 require "puma"
  88. rescue LoadError
  89. # Puma isn't installed (another server, or a command with "server" in its arguments)
  90. 1 STATE[:installed] = false
  91. 1 return
  92. end
  93. 1 puma_mod = ::Object.const_defined?(:Puma) ? ::Object.const_get(:Puma) : nil # rubocop:disable Sorbet/ConstantsFromStrings
  94. # rubocop:disable Sorbet/ConstantsFromStrings
  95. 1 if puma_mod&.const_defined?(:LogWriter)
  96. 1 ::Object.const_get("Puma::LogWriter").prepend(LogWriterPatch)
  97. end
  98. 1 if puma_mod&.const_defined?(:ErrorLogger)
  99. 1 ::Object.const_get("Puma::ErrorLogger").prepend(ErrorLoggerPatch)
  100. end
  101. 1 if puma_mod&.const_defined?(:Events)
  102. ev = ::Object.const_get("Puma::Events")
  103. ev.prepend(EventsPatch)
  104. end
  105. # Patch Rack::Handler::Puma.run to emit lifecycle logs using options
  106. 1 if ::Object.const_defined?(:Rack)
  107. 1 rack_mod = ::Object.const_get(:Rack)
  108. 1 if rack_mod.const_defined?(:Handler)
  109. handler_mod = rack_mod.const_get(:Handler)
  110. if handler_mod.const_defined?(:Puma)
  111. handler = handler_mod.const_get(:Puma)
  112. handler.singleton_class.prepend(RackHandlerPatch)
  113. end
  114. end
  115. end
  116. # Avoid patching CLI/Server; rely on log parsing
  117. # Avoid patching CLI to minimize version-specific risks
  118. # rubocop:enable Sorbet/ConstantsFromStrings
  119. rescue => e
  120. handle_integration_error(e)
  121. end
  122. # Rely on Puma patches to observe lines
  123. end
  124. 3 sig { params(e: StandardError).void }
  125. 2 def handle_integration_error(e)
  126. 1 server_mode = ::LogStruct.server_mode?
  127. 1 if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test? && !server_mode
  128. raise e
  129. else
  130. # LogStruct's own error: logged in production, so Puma keeps running
  131. 1 LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :puma})
  132. end
  133. end
  134. # No stdout interception
  135. 4 sig { void }
  136. 2 def state_reset!
  137. 28 STATE[:boot_emitted] = false
  138. 28 STATE[:shutdown_emitted] = false
  139. 28 STATE[:started_emitted] = false
  140. 28 STATE[:handler_pending_started] = false
  141. 28 STATE[:start_info] = {
  142. mode: nil,
  143. puma_version: nil,
  144. puma_codename: nil,
  145. ruby_version: nil,
  146. min_threads: nil,
  147. max_threads: nil,
  148. environment: nil,
  149. pid: nil,
  150. listening: []
  151. }
  152. end
  153. 3 sig { params(argv: T::Array[String]).returns(T.nilable(String)) }
  154. 2 def port_from_argv(argv)
  155. 1 argv.each_with_index do |arg, idx|
  156. 2 if arg == "-p" || arg == "--port"
  157. 1 return argv[idx + 1]
  158. 1 elsif arg.start_with?("--port=")
  159. return arg.split("=", 2)[1]
  160. end
  161. end
  162. nil
  163. end
  164. 3 sig { params(line: String).returns(T::Boolean) }
  165. 2 def process_line(line)
  166. 13 l = line.to_s.strip
  167. 13 return false if l.empty?
  168. # Suppress non-JSON rails banners
  169. 13 return true if l.start_with?("=> ")
  170. # Ignore boot line
  171. 12 return true if l.start_with?("=> Booting Puma")
  172. 12 if l.start_with?("Puma starting in ")
  173. # Example: Puma starting in single mode...
  174. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:mode] = l.sub("Puma starting in ", "").sub(" mode...", "")
  175. 1 return true
  176. end
  177. 11 if (m = l.match(/^(?:\*\s*)?Puma version: (\S+)(?:.*"([^\"]+)")?/))
  178. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_version] = m[1]
  179. 1 if m[2]
  180. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_codename] = m[2]
  181. end
  182. 1 return true
  183. end
  184. 10 if (m = l.match(/^\* Ruby version: (.+)$/))
  185. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:ruby_version] = m[1]
  186. 1 return true
  187. end
  188. 9 if (m = l.match(/^(?:\*\s*)?Min threads: (\d+)/))
  189. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:min_threads] = m[1].to_i
  190. 1 return true
  191. end
  192. 8 if (m = l.match(/^(?:\*\s*)?Max threads: (\d+)/))
  193. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:max_threads] = m[1].to_i
  194. 1 return true
  195. end
  196. 7 if (m = l.match(/^(?:\*\s*)?Environment: (\S+)/))
  197. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:environment] = m[1]
  198. 1 return true
  199. end
  200. 6 if (m = l.match(/^(?:\*\s*)?PID:\s+(\d+)/))
  201. 1 T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] = m[1].to_i
  202. 1 return true
  203. end
  204. 5 if (m = l.match(/^\*?\s*Listening on (.+)$/))
  205. 1 si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
  206. 1 list = T.cast(si[:listening], T::Array[T.untyped])
  207. 1 address = m[1].to_s
  208. 1 list << address unless list.include?(address)
  209. # Emit started when we see the first listening address
  210. 1 if !STATE[:started_emitted]
  211. 1 emit_started!
  212. 1 STATE[:started_emitted] = true
  213. end
  214. 1 return true
  215. end
  216. 4 if l == "Use Ctrl-C to stop"
  217. 1 si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
  218. # Fallback: if no listening address captured yet, infer from ARGV
  219. 1 if T.cast(si[:listening], T::Array[T.untyped]).empty?
  220. begin
  221. 1 port = port_from_argv(ARGV)
  222. 1 if port
  223. 1 si[:listening] << "tcp://127.0.0.1:#{port}"
  224. end
  225. rescue => e
  226. handle_integration_error(e)
  227. end
  228. end
  229. 1 if !STATE[:started_emitted]
  230. 1 emit_started!
  231. 1 STATE[:started_emitted] = true
  232. end
  233. 1 return false
  234. end
  235. 3 if l.start_with?("- Gracefully stopping")
  236. 1 emit_shutdown!(l)
  237. 1 return true
  238. end
  239. 2 if l.start_with?("=== puma shutdown:")
  240. emit_shutdown!(l)
  241. return true
  242. end
  243. 2 if l == "- Goodbye!"
  244. # Swallow
  245. 1 return true
  246. end
  247. 1 if l == "Exiting"
  248. emit_shutdown!(l)
  249. return true
  250. end
  251. 1 false
  252. end
  253. 2 sig { void }
  254. 2 def emit_boot_if_needed!
  255. # Intentionally no-op: we no longer emit a boot log
  256. STATE[:boot_emitted] = true
  257. end
  258. # No server hooks; rely on parsing only
  259. 3 sig { void }
  260. 2 def emit_started!
  261. 2 si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
  262. 2 log = Log::Puma::Start.new(
  263. mode: T.cast(si[:mode], T.nilable(String)),
  264. puma_version: T.cast(si[:puma_version], T.nilable(String)),
  265. puma_codename: T.cast(si[:puma_codename], T.nilable(String)),
  266. ruby_version: T.cast(si[:ruby_version], T.nilable(String)),
  267. min_threads: T.cast(si[:min_threads], T.nilable(Integer)),
  268. max_threads: T.cast(si[:max_threads], T.nilable(Integer)),
  269. environment: T.cast(si[:environment], T.nilable(String)),
  270. process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
  271. listening_addresses: T.cast(T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:listening], T::Array[String]),
  272. level: Level::Info,
  273. timestamp: Time.now
  274. )
  275. 2 LogStruct.info(log)
  276. 2 STATE[:handler_pending_started] = false
  277. # Only use LogStruct; SemanticLogger routes to STDOUT in test
  278. end
  279. # Logs the shutdown when the signal arrives, then runs the handler that was there before
  280. # (Puma's, which stops the server gracefully), or does what the signal does by default
  281. 3 sig { params(signal: String).void }
  282. 2 def trap_shutdown_signal(signal)
  283. 1 previous = T.let(nil, T.untyped)
  284. 1 previous = Signal.trap(signal) do |signal_number|
  285. 1 emit_shutdown!(signal)
  286. 1 run_previous_signal_handler(previous, signal, signal_number)
  287. end
  288. end
  289. 3 sig { params(previous: T.untyped, signal: String, signal_number: T.untyped).void }
  290. 2 def run_previous_signal_handler(previous, signal, signal_number)
  291. 1 if previous.respond_to?(:call)
  292. 1 (previous.respond_to?(:arity) && previous.arity == 0) ? previous.call : previous.call(signal_number)
  293. elsif previous == "EXIT"
  294. Kernel.exit
  295. elsif previous.nil? || previous == "DEFAULT" || previous == "SYSTEM_DEFAULT"
  296. # Put back the default and send the signal again, so the process ends as it would have
  297. Signal.trap(signal, previous || "DEFAULT")
  298. Process.kill(signal, Process.pid)
  299. end
  300. # "IGNORE": nothing more to do
  301. end
  302. 3 sig { params(_message: String).void }
  303. 2 def emit_shutdown!(_message)
  304. 1 return if STATE[:shutdown_emitted]
  305. 1 STATE[:shutdown_emitted] = true
  306. 1 log = Log::Puma::Shutdown.new(
  307. process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
  308. level: Level::Info,
  309. timestamp: Time.now
  310. )
  311. 1 LogStruct.info(log)
  312. # Only use LogStruct; SemanticLogger routes to STDOUT in test
  313. # Let SemanticLogger appender write to STDOUT
  314. end
  315. end
  316. # STDOUT interception is handled globally via StdoutFilter; keep Puma patches minimal
  317. # Patch Puma::LogWriter to intercept log writes
  318. 2 module LogWriterPatch
  319. 2 extend T::Sig
  320. 2 sig { params(msg: String).returns(T.untyped) }
  321. 2 def log(msg)
  322. consumed = ::LogStruct::Integrations::Puma.process_line(msg)
  323. super unless consumed
  324. end
  325. 2 sig { params(msg: String).returns(T.untyped) }
  326. 2 def write(msg)
  327. any_consumed = T.let(false, T::Boolean)
  328. msg.to_s.each_line do |l|
  329. any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
  330. end
  331. super unless any_consumed
  332. end
  333. 2 sig { params(msg: String).returns(T.untyped) }
  334. 2 def <<(msg)
  335. any_consumed = T.let(false, T::Boolean)
  336. msg.to_s.each_line do |l|
  337. any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
  338. end
  339. super unless any_consumed
  340. end
  341. 2 sig { params(msg: String).returns(T.untyped) }
  342. 2 def puts(msg)
  343. consumed = ::LogStruct::Integrations::Puma.process_line(msg)
  344. if consumed
  345. # attempt to suppress; only forward if not consumed
  346. return nil
  347. end
  348. if ::Kernel.instance_variables.include?(:@stdout)
  349. io = ::Kernel.instance_variable_get(:@stdout)
  350. return io.puts(msg)
  351. end
  352. super
  353. end
  354. 2 sig { params(msg: String).returns(T.untyped) }
  355. 2 def info(msg)
  356. consumed = ::LogStruct::Integrations::Puma.process_line(msg)
  357. super unless consumed
  358. end
  359. end
  360. # Puma::ErrorLogger writes request errors (an exception the app didn't handle, a malformed
  361. # request, an SSL error) as plain text to STDERR, with the request's query string. Log them
  362. # as structured lines, with the method and path but not the query string, which LogStruct
  363. # can't filter there.
  364. 2 module ErrorLoggerPatch
  365. 2 extend T::Sig
  366. 3 sig { params(options: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
  367. 2 def info(options = {})
  368. 2 error = options[:error]
  369. 2 return super unless error.is_a?(Exception)
  370. 2 env = options[:req]&.env
  371. 2 ::LogStruct.error(::LogStruct::Log::Puma::Error.new(
  372. error_class: error.class,
  373. message: error.message,
  374. backtrace: error.backtrace,
  375. error_context: options[:text]&.to_s,
  376. http_method: env&.dig("REQUEST_METHOD"),
  377. 2 path: env && (env["REQUEST_PATH"] || env["PATH_INFO"])
  378. ))
  379. 2 nil
  380. rescue => e
  381. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  382. super
  383. end
  384. end
  385. # Patch Puma::Events as a fallback for some versions where Events handles output
  386. 2 module EventsPatch
  387. 2 extend T::Sig
  388. 2 sig { params(str: String).returns(T.untyped) }
  389. 2 def log(str)
  390. consumed = ::LogStruct::Integrations::Puma.process_line(str)
  391. super unless consumed
  392. end
  393. end
  394. # Hook Rack::Handler::Puma.run to emit structured started/shutdown
  395. 2 module RackHandlerPatch
  396. 2 extend T::Sig
  397. 2 sig do
  398. params(
  399. app: T.untyped,
  400. args: T.untyped,
  401. block: T.nilable(T.proc.returns(T.untyped))
  402. ).returns(T.untyped)
  403. end
  404. 2 def run(app, *args, &block)
  405. rest = args
  406. options = T.let({}, T::Hash[T.untyped, T.untyped])
  407. rest.each do |value|
  408. next unless value.is_a?(Hash)
  409. options.merge!(value)
  410. end
  411. begin
  412. si = T.cast(::LogStruct::Integrations::Puma::STATE[:start_info], T::Hash[Symbol, T.untyped])
  413. si[:mode] ||= "single"
  414. si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
  415. si[:pid] ||= Process.pid
  416. si[:listening] ||= []
  417. port = T.let(nil, T.untyped)
  418. host = T.let(nil, T.untyped)
  419. if options.respond_to?(:[])
  420. port = options[:Port] || options["Port"] || options[:port] || options["port"]
  421. host = options[:Host] || options["Host"] || options[:host] || options["host"]
  422. end
  423. if port
  424. list = T.cast(si[:listening], T::Array[T.untyped])
  425. list.clear
  426. h = (host && host != "0.0.0.0") ? host : "127.0.0.1"
  427. list << "tcp://#{h}:#{port}"
  428. end
  429. state = ::LogStruct::Integrations::Puma::STATE
  430. state[:handler_pending_started] = true unless state[:started_emitted]
  431. rescue => e
  432. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  433. end
  434. begin
  435. Kernel.at_exit do
  436. unless ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted]
  437. ::LogStruct::Integrations::Puma.emit_shutdown!("Exiting")
  438. ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted] = true
  439. end
  440. rescue => e
  441. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  442. end
  443. rescue => e
  444. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  445. end
  446. begin
  447. result = super(app, **options, &block)
  448. ensure
  449. state = ::LogStruct::Integrations::Puma::STATE
  450. if state[:handler_pending_started] && !state[:started_emitted]
  451. begin
  452. ::LogStruct::Integrations::Puma.emit_started!
  453. state[:started_emitted] = true
  454. rescue => e
  455. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  456. ensure
  457. state[:handler_pending_started] = false
  458. end
  459. end
  460. end
  461. result
  462. end
  463. end
  464. # (No Launcher patch)
  465. # No Server patch
  466. # No InterceptorIO
  467. # Removed EventsInitPatch and CLIPatch to avoid version-specific conflicts
  468. end
  469. end
  470. end

lib/log_struct/integrations/rack_error_handler.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "rack"
  4. 2 require "action_dispatch/middleware/debug_exceptions"
  5. 2 require_relative "rack_error_handler/middleware"
  6. 2 require_relative "rack_error_handler/debug_exceptions_logging"
  7. 2 require_relative "rack_error_handler/parse_error_logging"
  8. 2 require_relative "rack_setup"
  9. 2 module LogStruct
  10. 2 module Integrations
  11. # Rack middleware integration for structured logging
  12. 2 module RackErrorHandler
  13. 2 extend T::Sig
  14. 2 extend IntegrationInterface
  15. 2 @interceptor_registered = T.let(false, T::Boolean)
  16. # Set up structured lines for CSRF and IP spoofing errors, errors the app didn't handle, and
  17. # errors that get past ShowExceptions
  18. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  19. 2 def self.setup(config)
  20. 2 return nil unless RackSetup.enabled?(config)
  21. # Errors from the middleware outside ShowExceptions, and any that get past it
  22. 2 RackSetup.insert_before(
  23. ::ActionDispatch::RemoteIp,
  24. Integrations::RackErrorHandler::Middleware
  25. )
  26. # DebugExceptions calls its interceptors with each error from the app (in every environment),
  27. # before ShowExceptions turns it into Rails' usual response: 422 for a CSRF error
  28. 2 unless @interceptor_registered
  29. 2 ::ActionDispatch::DebugExceptions.register_interceptor(ErrorLines.method(:intercept))
  30. 2 @interceptor_registered = true
  31. end
  32. # An error line instead of the text Rails logs for an error the app didn't handle
  33. 2 unless ::ActionDispatch::DebugExceptions <= DebugExceptionsLogging
  34. 2 ::ActionDispatch::DebugExceptions.prepend(DebugExceptionsLogging)
  35. end
  36. # The size of a body Rails can't parse, instead of the body
  37. 2 ::ActionDispatch::Request.prepend(ParseErrorLogging) unless ::ActionDispatch::Request <= ParseErrorLogging
  38. 2 true
  39. end
  40. end
  41. end
  42. end

lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb

68.75% lines covered

16 relevant lines. 11 lines covered and 5 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "error_lines"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. 2 module RackErrorHandler
  7. # Prepended to ActionDispatch::DebugExceptions. For an error the app didn't handle, Rails logs a
  8. # block of text (the error, its source code and its trace) before ShowExceptions renders the
  9. # error page. LogStruct writes an error line instead.
  10. 2 module DebugExceptionsLogging
  11. 2 extend T::Sig
  12. 2 extend T::Helpers
  13. 2 requires_ancestor { ::ActionDispatch::DebugExceptions }
  14. 2 private
  15. 2 sig { params(request: ::ActionDispatch::Request, wrapper: T.untyped).returns(T.untyped) }
  16. 2 def log_error(request, wrapper)
  17. return super unless ErrorLines.enabled?
  18. # As Rails does: config.action_dispatch.log_rescued_responses = false leaves out the errors
  19. # Rails turns into a response of their own (404 for a routing error, 422 for CSRF)
  20. return if !request.get_header("action_dispatch.log_rescued_responses") && wrapper.rescue_response?
  21. ErrorLines.log_unhandled(request, wrapper)
  22. rescue => e
  23. LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :rack_error_handler})
  24. super
  25. end
  26. end
  27. end
  28. end
  29. end

lib/log_struct/integrations/rack_error_handler/error_lines.rb

89.83% lines covered

59 relevant lines. 53 lines covered and 6 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../../params_errors"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. 2 module RackErrorHandler
  7. # The structured lines for errors raised while Rails handles a request. Each error is logged once:
  8. # security errors from the app by DebugExceptions' interceptor, anything else that gets past
  9. # ShowExceptions by the middleware. Rails still turns each error into its usual response.
  10. 2 module ErrorLines
  11. 2 extend T::Sig
  12. 2 CSRF_ERROR_NAMES = T.let(
  13. ["ActionController::InvalidAuthenticityToken", "ActionController::InvalidCrossOriginRequest"].freeze,
  14. T::Array[String]
  15. )
  16. 3 sig { returns(T::Boolean) }
  17. 2 def self.enabled?
  18. 6 LogStruct.enabled? && LogStruct.config.integrations.enable_rack_error_handler
  19. end
  20. # For DebugExceptions.register_interceptor: CSRF and IP spoofing errors from the app, which
  21. # ShowExceptions would otherwise turn into a response before any middleware saw them.
  22. # Other errors are left to Rails' own logging and error reporting.
  23. 3 sig { params(request: ::ActionDispatch::Request, error: Exception).void }
  24. 2 def self.intercept(request, error)
  25. 2 return unless enabled?
  26. 2 return unless security_error?(error)
  27. 2 return if LoggedErrors.logged?(error)
  28. 2 log_security_error(request, error)
  29. end
  30. # An error that got past ShowExceptions (or came from a middleware outside it)
  31. 3 sig { params(request: ::ActionDispatch::Request, error: StandardError).void }
  32. 2 def self.log_escaped(request, error)
  33. 3 return unless enabled?
  34. 3 return if LoggedErrors.logged?(error)
  35. 2 return log_security_error(request, error) if security_error?(error)
  36. 2 LoggedErrors.mark(error)
  37. 2 LogStruct.error(
  38. Log::Error.new(
  39. source: Source::Rails,
  40. error_class: error.class,
  41. message: ParamsErrors.messages(error).first,
  42. backtrace: error.backtrace,
  43. path: request.path,
  44. http_method: request.method,
  45. source_ip: source_ip(request),
  46. user_agent: request.user_agent,
  47. referer: StringScrubber.without_query(request.referer)
  48. )
  49. )
  50. end
  51. # An error the app didn't handle, which DebugExceptions logs before ShowExceptions renders Rails'
  52. # error page: at config.action_dispatch.debug_exception_log_level (fatal by default), with the
  53. # trace Rails would log and the response's status. A security error already has its own line.
  54. 3 sig { params(request: ::ActionDispatch::Request, wrapper: T.untyped).void }
  55. 2 def self.log_unhandled(request, wrapper)
  56. 4 error = T.cast(wrapper.exception, Exception)
  57. 4 return if LoggedErrors.logged?(error)
  58. 4 LoggedErrors.mark(error)
  59. # Without a backtrace cleaner, Rails' trace is Thread::Backtrace::Locations
  60. 4 trace = Array(wrapper.exception_trace).map(&:to_s)
  61. 4 message, cause_message = ParamsErrors.messages(error)
  62. 4 line = Log::Error.new(
  63. source: Source::Rails,
  64. error_class: error.class,
  65. message: message,
  66. 4 backtrace: trace.empty? ? error.backtrace : trace,
  67. path: request.path,
  68. http_method: request.method,
  69. source_ip: source_ip(request),
  70. user_agent: request.user_agent,
  71. referer: StringScrubber.without_query(request.referer),
  72. status: wrapper.status_code,
  73. cause_class: error.cause&.class&.name,
  74. cause_message: cause_message
  75. )
  76. 4 case request.get_header("action_dispatch.debug_exception_log_level")
  77. when ::Logger::DEBUG then LogStruct.debug(line)
  78. when ::Logger::INFO then LogStruct.info(line)
  79. when ::Logger::WARN then LogStruct.warn(line)
  80. when ::Logger::ERROR then LogStruct.error(line)
  81. 4 else LogStruct.fatal(line)
  82. end
  83. end
  84. # A security line, and the error handled in the security_errors mode (reported by default)
  85. 3 sig { params(request: ::ActionDispatch::Request, error: Exception).void }
  86. 2 def self.log_security_error(request, error)
  87. 2 LoggedErrors.mark(error)
  88. 2 if ip_spoof_error?(error)
  89. LogStruct.warn(
  90. Log::Security::IPSpoof.new(
  91. path: request.path,
  92. http_method: request.method,
  93. user_agent: request.user_agent,
  94. referer: StringScrubber.without_query(request.referer),
  95. message: error.message,
  96. source_ip: request.ip,
  97. client_ip: request.get_header("HTTP_CLIENT_IP"),
  98. x_forwarded_for: request.x_forwarded_for,
  99. timestamp: Time.now
  100. )
  101. )
  102. else
  103. 2 LogStruct.error(
  104. Log::Security::CSRFViolation.new(
  105. path: request.path,
  106. http_method: request.method,
  107. source_ip: source_ip(request),
  108. user_agent: request.user_agent,
  109. referer: StringScrubber.without_query(request.referer),
  110. message: error.message,
  111. timestamp: Time.now
  112. )
  113. )
  114. end
  115. 2 reported = error.is_a?(StandardError) ? error : RuntimeError.new(error.message)
  116. 2 LogStruct.handle_exception(reported, source: Source::Security, context: request_context(request), log: false)
  117. end
  118. 3 sig { params(request: ::ActionDispatch::Request).returns(T::Hash[Symbol, T.untyped]) }
  119. 2 def self.request_context(request)
  120. {
  121. 2 path: request.path,
  122. method: request.method,
  123. user_agent: request.user_agent,
  124. referer: StringScrubber.without_query(request.referer)
  125. }
  126. end
  127. # The client's IP, as Rails works it out, or the connection's when Rails finds a spoofed one
  128. 3 sig { params(request: ::ActionDispatch::Request).returns(T.nilable(String)) }
  129. 2 def self.source_ip(request)
  130. 8 request.remote_ip
  131. rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError
  132. request.ip
  133. end
  134. 3 sig { params(error: Exception).returns(T::Boolean) }
  135. 2 def self.security_error?(error)
  136. 8 ip_spoof_error?(error) || CSRF_ERROR_NAMES.include?(error.class.name)
  137. end
  138. 3 sig { params(error: Exception).returns(T::Boolean) }
  139. 2 def self.ip_spoof_error?(error)
  140. 10 error.is_a?(::ActionDispatch::RemoteIp::IpSpoofAttackError)
  141. end
  142. end
  143. end
  144. end
  145. end

lib/log_struct/integrations/rack_error_handler/middleware.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../rack_middleware"
  4. 2 require_relative "error_lines"
  5. 2 module LogStruct
  6. 2 module Integrations
  7. 2 module RackErrorHandler
  8. # Logs the errors that get past ShowExceptions, or come from the middleware outside it (an IP
  9. # spoofing error from a middleware that reads remote_ip, for example), then raises them again
  10. # for the server to handle as it would without LogStruct
  11. 2 class Middleware < RackMiddleware
  12. 3 sig { override.params(env: T.untyped).returns(T.untyped) }
  13. 2 def call(env)
  14. 2 @app.call(env)
  15. rescue => error
  16. begin
  17. 2 ErrorLines.log_escaped(::ActionDispatch::Request.new(env), error)
  18. rescue => logging_error
  19. LogStruct.handle_exception(logging_error, source: Source::Internal)
  20. end
  21. 2 raise error
  22. end
  23. end
  24. end
  25. end
  26. end

lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module RackErrorHandler
  6. # Prepended to ActionDispatch::Request. When Rails can't parse a request's body (malformed JSON), it logs the
  7. # whole body at debug level, where nothing filters it. LogStruct logs the body's size and type instead. The
  8. # request line and the ParseError's error line are written as usual.
  9. 2 module ParseErrorLogging
  10. 2 extend T::Sig
  11. 2 extend T::Helpers
  12. 2 requires_ancestor { ::ActionDispatch::Request }
  13. 2 private
  14. 3 sig { returns(T.untyped) }
  15. 2 def log_parse_error_once
  16. 1 return super unless ErrorLines.enabled?
  17. 1 @logstruct_parse_error_logged ||= T.let(
  18. begin
  19. 1 (logger || ::ActiveSupport::Logger.new($stderr)).debug(
  20. "Error occurred while parsing request parameters. LogStruct leaves out the body " \
  21. "(#{raw_post.to_s.bytesize} bytes of #{media_type}), which it can't filter."
  22. )
  23. 1 true
  24. end,
  25. T.nilable(TrueClass)
  26. )
  27. end
  28. end
  29. end
  30. end
  31. end

lib/log_struct/integrations/rack_middleware.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. # A Rack middleware around the app: subclasses define call, and pass the request on to @app
  6. 2 class RackMiddleware
  7. 2 extend T::Sig
  8. 2 extend T::Helpers
  9. 2 abstract!
  10. 4 sig { params(app: T.untyped).void }
  11. 2 def initialize(app)
  12. 5 @app = app
  13. end
  14. 3 sig { abstract.params(env: T.untyped).returns(T.untyped) }
  15. 2 def call(env)
  16. end
  17. end
  18. end
  19. end

lib/log_struct/integrations/rack_setup.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module RackSetup
  6. 2 extend T::Sig
  7. 4 sig { params(config: LogStruct::Configuration).returns(T::Boolean) }
  8. 2 def self.enabled?(config)
  9. 2 return false unless config.enabled
  10. 2 return false unless config.integrations.enable_rack_error_handler
  11. 2 true
  12. end
  13. 2 sig { params(anchor: T.untyped, middleware: T.untyped).void }
  14. 2 def self.insert_after(anchor, middleware)
  15. ::Rails.application.middleware.insert_after(anchor, middleware)
  16. end
  17. 4 sig { params(anchor: T.untyped, middleware: T.untyped).void }
  18. 2 def self.insert_before(anchor, middleware)
  19. 2 ::Rails.application.middleware.insert_before(anchor, middleware)
  20. end
  21. end
  22. end
  23. end

lib/log_struct/integrations/rails_log_subscribers.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. # Rails' own log subscribers, which LogStruct's lines replace
  6. 2 module RailsLogSubscribers
  7. 2 extend T::Sig
  8. # Detaches a framework's log subscriber (ActiveStorage::LogSubscriber, :active_storage). Up to Rails 8.1 it's an
  9. # ActiveSupport::LogSubscriber attached to the framework's notifications; from Rails 8.2 it's an
  10. # ActiveSupport::EventReporter::LogSubscriber, subscribed to Rails.event when its file loads.
  11. 4 sig { params(subscriber_class: T.untyped, namespace: Symbol).void }
  12. 2 def self.detach(subscriber_class, namespace)
  13. 14 if subscriber_class.respond_to?(:detach_from)
  14. 10 subscriber_class.detach_from(namespace)
  15. else
  16. 4 unsubscribe_from_events(subscriber_class)
  17. end
  18. end
  19. # Unsubscribes a Rails 8.2 log subscriber from Rails.event. Does nothing for one that isn't subscribed there.
  20. 4 sig { params(subscriber_class: T.untyped).void }
  21. 2 def self.unsubscribe_from_events(subscriber_class)
  22. 6 return unless ::ActiveSupport.respond_to?(:event_reporter)
  23. 6 reporter = ::ActiveSupport.event_reporter
  24. 6 reporter.unsubscribe(subscriber_class) if reporter.respond_to?(:unsubscribe)
  25. end
  26. end
  27. end
  28. end

lib/log_struct/integrations/request_context.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "request_context/middleware"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. # Request context integration that captures request_id for all logs
  7. 2 module RequestContext
  8. 2 extend T::Sig
  9. 2 extend IntegrationInterface
  10. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  11. 2 def self.setup(config)
  12. 2 return nil unless config.enabled
  13. # Insert after RequestId middleware so request_id is available
  14. 2 ::Rails.application.middleware.insert_after(
  15. ::ActionDispatch::RequestId,
  16. Integrations::RequestContext::Middleware
  17. )
  18. 2 true
  19. end
  20. end
  21. end
  22. end

lib/log_struct/integrations/request_context/middleware.rb

50.0% lines covered

14 relevant lines. 7 lines covered and 7 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../rack_middleware"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. 2 module RequestContext
  7. # Middleware that captures request_id and stores it in SemanticLogger's
  8. # named_tags so all logs during the request include the request_id.
  9. 2 class Middleware < RackMiddleware
  10. 2 sig { override.params(env: T.untyped).returns(T.untyped) }
  11. 2 def call(env)
  12. request = ::ActionDispatch::Request.new(env)
  13. request_id = request.request_id
  14. Thread.current[:logstruct_request_id] = request_id
  15. ::SemanticLogger.push_named_tags(request_id: request_id)
  16. @app.call(env)
  17. ensure
  18. ::SemanticLogger.pop_named_tags
  19. Thread.current[:logstruct_request_id] = nil
  20. end
  21. end
  22. end
  23. end
  24. end

lib/log_struct/integrations/shrine.rb

50.0% lines covered

52 relevant lines. 26 lines covered and 26 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "shrine"
  5. rescue LoadError
  6. # Shrine gem is not available, integration will be skipped
  7. end
  8. 2 module LogStruct
  9. 2 module Integrations
  10. # Shrine integration for structured logging
  11. 2 module Shrine
  12. 2 extend T::Sig
  13. 2 extend IntegrationInterface
  14. 2 SHRINE_EVENTS = T.let(%i[upload exists download delete metadata open].freeze, T::Array[Symbol])
  15. # The file name in a Content-Disposition (attachment; filename="..."; filename*=UTF-8''...). A quoted name
  16. # can hold a semicolon, and whatever follows a quote (malformed or not) goes too.
  17. 2 DISPOSITION_FILENAME_REGEX = /(\bfilename\*?\s*=\s*)(?:"(?:[^"\\]|\\.)*"?)?[^;]*/i
  18. # Set up Shrine structured logging
  19. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  20. 2 def self.setup(config)
  21. 2 return nil unless defined?(::Shrine)
  22. 2 return nil unless config.enabled
  23. 2 return nil unless config.integrations.enable_shrine
  24. # Create a structured log subscriber for Shrine
  25. # ActiveSupport::Notifications::Event has name, time, end, transaction_id, payload, and duration
  26. 2 shrine_log_subscriber = lambda do |event|
  27. payload = event.payload.except(:io, :name).dup
  28. # Map event name to Event type
  29. event_type = case event.name
  30. when :upload then Event::Upload
  31. when :download then Event::Download
  32. when :open then Event::Download
  33. when :delete then Event::Delete
  34. when :exists then Event::Exist
  35. else Event::Metadata
  36. end
  37. # Create structured log data
  38. # Ensure storage is always a symbol
  39. storage_sym = payload[:storage].to_sym
  40. log_data = case event_type
  41. when Event::Upload
  42. metadata = payload[:metadata].is_a?(Hash) ? payload[:metadata] : {}
  43. size = metadata["size"]
  44. Log::Shrine::Upload.new(
  45. storage: storage_sym,
  46. location: payload[:location],
  47. mime_type: metadata["mime_type"]&.to_s,
  48. size: size.is_a?(Integer) ? size : nil,
  49. uploader: payload[:uploader]&.to_s,
  50. upload_options: without_file_names(payload[:upload_options]),
  51. options: payload[:options],
  52. duration_ms: event.duration.to_f
  53. )
  54. when Event::Download
  55. Log::Shrine::Download.new(
  56. storage: storage_sym,
  57. location: payload[:location],
  58. download_options: payload[:download_options]
  59. )
  60. when Event::Delete
  61. Log::Shrine::Delete.new(
  62. storage: storage_sym,
  63. location: payload[:location]
  64. )
  65. when Event::Exist
  66. Log::Shrine::Exist.new(
  67. storage: storage_sym,
  68. location: payload[:location]
  69. )
  70. else
  71. # metadata, the only other event in SHRINE_EVENTS
  72. Log::Shrine::Metadata.new(storage: storage_sym, duration_ms: event.duration.to_f)
  73. end
  74. # Log directly through SemanticLogger, NOT through Shrine.logger
  75. # Shrine.logger is a basic Logger that would just call .to_s on the struct
  76. ::SemanticLogger[::Shrine].info(log_data)
  77. end
  78. # Load the instrumentation plugin without Shrine's default log subscriber, which logs
  79. # each event as a line of text (a no-op if the app has loaded it already). Shrine
  80. # ignores a subscriber that's already subscribed.
  81. 2 ::Shrine.plugin :instrumentation, log_subscriber: nil
  82. 2 SHRINE_EVENTS.each do |event_name|
  83. 12 ::Shrine.subscribe(event_name, &shrine_log_subscriber)
  84. end
  85. # The app may load the plugin with the default log subscriber too, before or after this
  86. 2 ::Shrine::Plugins::Instrumentation::LogSubscriber.singleton_class.prepend(SkipLoggedEvents)
  87. # Other plugins' log subscribers and Shrine's warnings write text to Shrine.logger
  88. 2 ::Shrine.logger = ::SemanticLogger[::Shrine]
  89. 2 true
  90. end
  91. # Upload options with the file names taken out of a Content-Disposition (upload_options:
  92. # {content_disposition: ...}, which the upload_options plugin often sets): they can name a person
  93. 2 sig { params(options: T.untyped).returns(T.untyped) }
  94. 2 def self.without_file_names(options)
  95. return options unless options.is_a?(Hash)
  96. options.to_h do |key, value|
  97. if value.is_a?(String) && KeyWords.normalize(key).include?("disposition")
  98. [key, value.gsub(DISPOSITION_FILENAME_REGEX, '\1[FILENAME]')]
  99. else
  100. [key, value]
  101. end
  102. end
  103. end
  104. # Prepended to Shrine's default instrumentation log subscriber, so it doesn't log the
  105. # events that LogStruct logs
  106. 2 module SkipLoggedEvents
  107. 2 extend T::Sig
  108. 2 sig { params(event: T.untyped).returns(T.untyped) }
  109. 2 def call(event)
  110. return if SHRINE_EVENTS.include?(event.name)
  111. super
  112. end
  113. end
  114. end
  115. end
  116. end

lib/log_struct/integrations/sidekiq.rb

91.53% lines covered

59 relevant lines. 54 lines covered and 5 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 2 require "sidekiq"
  5. rescue LoadError
  6. # Sidekiq gem is not available, integration will be skipped
  7. end
  8. 2 if defined?(::Sidekiq)
  9. 1 require_relative "sidekiq/logger"
  10. 1 require_relative "sidekiq/job_context_middleware"
  11. end
  12. 2 module LogStruct
  13. 2 module Integrations
  14. # Sidekiq integration for structured logging
  15. 2 module Sidekiq
  16. 2 extend T::Sig
  17. 2 extend IntegrationInterface
  18. 2 @original_logger = T.let(nil, T.untyped)
  19. # Set up Sidekiq structured logging
  20. 3 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  21. 2 def self.setup(config)
  22. 2 return nil unless defined?(::Sidekiq)
  23. 2 return nil unless config.enabled
  24. 2 return nil unless config.integrations.enable_sidekiq
  25. 1 configure_logger
  26. 1 ::Sidekiq.configure_server do |sidekiq_config|
  27. # First in the chain, so lines logged by other middleware include the job too
  28. sidekiq_config.server_middleware do |chain|
  29. chain.prepend LogStruct::Integrations::Sidekiq::JobContextMiddleware
  30. end
  31. replace_default_error_handler(sidekiq_config)
  32. end
  33. 1 true
  34. end
  35. # Use LogStruct's logger for Sidekiq's own lines. The Railtie calls this as soon as it replaces
  36. # Rails.logger, before config/initializers run, because an initializer that configures Sidekiq
  37. # (connecting to Redis, setting up a fetcher) logs with Sidekiq's logger straight away.
  38. 4 sig { void }
  39. 2 def self.configure_logger
  40. 5 return unless defined?(::Sidekiq)
  41. 4 current = sidekiq_logger_config.logger
  42. 4 return if current.is_a?(LogStruct::Integrations::Sidekiq::Logger)
  43. 3 @original_logger = current
  44. 3 name = ::Sidekiq.server? ? "Sidekiq-Server" : "Sidekiq-Client"
  45. 3 sidekiq_logger_config.logger = LogStruct::Integrations::Sidekiq::Logger.new(name)
  46. end
  47. # Put Sidekiq's own logger back, when an initializer turned LogStruct or this integration off
  48. # after configure_logger ran
  49. 4 sig { void }
  50. 2 def self.restore_logger
  51. 2 return unless defined?(::Sidekiq)
  52. 1 return if @original_logger.nil?
  53. 1 sidekiq_logger_config.logger = @original_logger
  54. 1 @original_logger = nil
  55. end
  56. # Sidekiq 7 keeps the logger on its default configuration, Sidekiq 6 on the module
  57. 3 sig { returns(T.untyped) }
  58. 2 def self.sidekiq_logger_config
  59. 8 ::Sidekiq.respond_to?(:default_configuration) ? ::Sidekiq.default_configuration : ::Sidekiq
  60. end
  61. # Sidekiq's default error handler logs three lines for each failed job: the job (with its
  62. # arguments) as a JSON string, the error, and the backtrace. Log one structured line instead,
  63. # without the arguments, which LogStruct can't filter inside a JSON string.
  64. 3 sig { params(sidekiq_config: T.untyped).void }
  65. 2 def self.replace_default_error_handler(sidekiq_config)
  66. 4 return unless defined?(::Sidekiq::Config::ERROR_HANDLER)
  67. 4 handlers = sidekiq_config.error_handlers
  68. 4 return unless handlers.delete(::Sidekiq::Config::ERROR_HANDLER)
  69. 1 handlers.unshift(JOB_ERROR_HANDLER)
  70. end
  71. # The line has the job's ID and class in ctx, like the lines logged while the job ran (Sidekiq
  72. # calls its error handlers after the job's middleware has finished).
  73. 3 sig { params(error: Exception, context: T::Hash[T.untyped, T.untyped]).void }
  74. 2 def self.log_job_error(error, context)
  75. # LogStruct's ActiveJob integration logs the errors it sees an ActiveJob job raise. An
  76. # error raised before ActiveJob ran the job (a job class a deploy removed, say) is logged
  77. # here.
  78. 3 return if LogStruct::LoggedErrors.logged?(error)
  79. 2 job = context[:job]
  80. 2 job = nil unless job.is_a?(Hash)
  81. 2 error_context = context[:context]
  82. 2 line = Log::Sidekiq::Error.new(
  83. error_class: error.class,
  84. message: error.message,
  85. backtrace: error.backtrace,
  86. error_context: error_context&.to_s,
  87. queue_name: job&.dig("queue")&.to_s,
  88. retry_count: job&.dig("retry_count")
  89. )
  90. 2 if job.nil?
  91. ::Rails.logger.warn(line)
  92. return
  93. end
  94. job_context = {
  95. 2 jid: job["jid"],
  96. class: job["display_class"] || job["wrapped"] || job["class"]
  97. }.compact
  98. 4 ::SemanticLogger.named_tagged(sidekiq_context: job_context) { ::Rails.logger.warn(line) }
  99. end
  100. 2 JOB_ERROR_HANDLER = T.let(
  101. 3 ->(error, context, _config = nil) { log_job_error(error, context) },
  102. T.proc.params(error: Exception, context: T::Hash[T.untyped, T.untyped], config: T.untyped).void
  103. )
  104. 2 private_class_method :sidekiq_logger_config, :replace_default_error_handler, :log_job_error
  105. end
  106. end
  107. end

lib/log_struct/integrations/sidekiq/job_context_middleware.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 module LogStruct
  4. 1 module Integrations
  5. 1 module Sidekiq
  6. # Sidekiq server middleware that adds the running job's ID and class to every line
  7. # logged while it runs, in the same `ctx` field as Sidekiq's own log lines. A query
  8. # on ctx.jid then returns every line for one job run.
  9. 1 class JobContextMiddleware
  10. 1 extend T::Sig
  11. 1 sig do
  12. 1 params(
  13. job_instance: T.untyped,
  14. job_hash: T::Hash[String, T.untyped],
  15. queue: String,
  16. block: T.proc.returns(T.untyped)
  17. ).returns(T.untyped)
  18. end
  19. 1 def call(job_instance, job_hash, queue, &block)
  20. context = {
  21. 2 jid: job_hash["jid"],
  22. # Same precedence as Sidekiq's own context, so ActiveJob jobs show their class
  23. class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
  24. }
  25. 2 ::SemanticLogger.named_tagged(sidekiq_context: context, &block)
  26. end
  27. end
  28. end
  29. end
  30. end

lib/log_struct/integrations/sidekiq/logger.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 require_relative "../../semantic_logger/logger"
  4. 1 require_relative "../../log/sidekiq"
  5. 1 require_relative "../../enums/source"
  6. 1 module LogStruct
  7. 1 module Integrations
  8. 1 module Sidekiq
  9. 1 extend T::Sig
  10. # Get thread ID for Sidekiq logging
  11. 2 sig { returns(String) }
  12. 1 def self.tid
  13. 3 Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
  14. end
  15. # Custom Logger for Sidekiq that creates LogStruct::Log::Sidekiq::Log entries
  16. 1 class Logger < LogStruct::SemanticLogger::Logger
  17. 1 extend T::Sig
  18. # Override log methods to create Sidekiq log structs
  19. 1 %i[debug info warn error fatal].each do |level|
  20. 5 define_method(level) do |message = nil, payload = nil, &block|
  21. # Hashes, LogStruct log entries and exceptions are logged as they are, so their keys are
  22. # filtered (Sidekiq logs some settings as a Hash)
  23. 5 unless message.nil? || message.is_a?(String)
  24. 1 next super(message, payload, &block)
  25. end
  26. # A message with an exception is an error line with both
  27. 4 next super(message, payload, &block) if payload.is_a?(Exception)
  28. 3 msg = message || (block ? block.call.to_s : "")
  29. # Lines logged outside a job have no context
  30. 3 context = ::Sidekiq::Context.current
  31. # Create a Sidekiq log struct with the message
  32. 3 log_struct = Log::Sidekiq::Log.new(
  33. level: LogStruct::Level.from_severity(level.to_s.upcase),
  34. event: Event::Log,
  35. message: msg,
  36. process_id: ::Process.pid,
  37. thread_id: Sidekiq.tid,
  38. # Log a copy: SemanticLogger may still be serializing the struct on its appender
  39. # thread when Sidekiq adds :elapsed to the live context after the job finishes.
  40. 3 context: context.blank? ? nil : context.dup,
  41. 3 additional_data: payload.is_a?(Hash) ? payload : nil
  42. )
  43. # Pass the struct to SemanticLogger
  44. 3 super(log_struct, nil, &nil)
  45. end
  46. end
  47. end
  48. end
  49. end
  50. end

lib/log_struct/integrations/sorbet.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "sorbet-runtime"
  4. 2 module LogStruct
  5. 2 module Integrations
  6. # Integration for Sorbet runtime type checking error handlers
  7. # This module installs error handlers that report type errors through LogStruct
  8. # These handlers can be enabled/disabled using configuration
  9. 2 module Sorbet
  10. 2 extend T::Sig
  11. 2 extend IntegrationInterface
  12. # Set up Sorbet error handlers to report errors through LogStruct
  13. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  14. 2 def self.setup(config)
  15. 12 return nil unless config.integrations.enable_sorbet_error_handlers
  16. # Install inline type error handler
  17. # Called when T.let, T.cast, T.must, etc. fail
  18. 11 T::Configuration.inline_type_error_handler = lambda do |error, _opts|
  19. 2 add_backtrace(error)
  20. 2 LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  21. end
  22. # Install call validation error handler
  23. # Called when method signature validation fails
  24. 11 T::Configuration.call_validation_error_handler = lambda do |_signature, opts|
  25. 10 error = TypeError.new(opts[:pretty_message])
  26. 10 add_backtrace(error)
  27. 10 LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  28. end
  29. # Install sig builder error handler
  30. # Called when there's a problem with a signature definition
  31. 11 T::Configuration.sig_builder_error_handler = lambda do |error, _location|
  32. 1 add_backtrace(error)
  33. 1 LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  34. end
  35. # Install sig validation error handler
  36. # Called when there's a problem with a signature validation
  37. 11 T::Configuration.sig_validation_error_handler = lambda do |error, _opts|
  38. 1 add_backtrace(error)
  39. 1 LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  40. end
  41. 11 true
  42. end
  43. # Starts the error's backtrace at the code that failed, below sorbet-runtime's frames (and this file's).
  44. # sorbet-runtime passes some errors without raising them, which have no backtrace at all.
  45. 3 sig { params(error: Exception).void }
  46. 2 def self.add_backtrace(error)
  47. 14 frames = error.backtrace || caller
  48. 131 error.set_backtrace(frames.drop_while { |frame| frame.include?(__FILE__) || frame.include?("sorbet-runtime") })
  49. end
  50. end
  51. end
  52. end

lib/log_struct/log.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # Common enums and shared interfaces
  4. 2 require_relative "enums/source"
  5. 2 require_relative "enums/event"
  6. 2 require_relative "enums/level"
  7. 2 require_relative "enums/log_field"
  8. 2 require_relative "log/interfaces/public_common_fields"
  9. 2 require_relative "shared/serialize_common_public"
  10. 2 require_relative "params_errors"
  11. # Dynamically require all top-level log structs under log/*
  12. # Nested per-event files are required by their parent files.
  13. 2 Dir[File.join(__dir__, "log", "*.rb")].sort.each do |file|
  14. 32 require file
  15. end
  16. 2 module LogStruct
  17. 2 module Log
  18. 2 extend T::Sig
  19. # Build an Error log from an exception with optional context and timestamp
  20. 2 sig do
  21. 1 params(
  22. source: Source,
  23. ex: Exception,
  24. additional_data: T::Hash[T.any(String, Symbol), T.untyped],
  25. timestamp: Time
  26. ).returns(LogStruct::Log::Error)
  27. end
  28. 2 def self.from_exception(source, ex, additional_data = {}, timestamp = Time.now)
  29. 21 LogStruct::Log::Error.new(
  30. source: source,
  31. error_class: ex.class,
  32. # Without the input a params parser quoted, for a request's params error that the app logs itself
  33. message: ParamsErrors.messages(ex).first,
  34. backtrace: ex.backtrace,
  35. additional_data: additional_data,
  36. timestamp: timestamp
  37. )
  38. end
  39. end
  40. end

lib/log_struct/log/action_mailer.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "action_mailer/delivery"
  8. 2 require_relative "action_mailer/delivered"
  9. 2 require_relative "action_mailer/error"
  10. 2 module LogStruct
  11. 2 module Log
  12. 2 class ActionMailer
  13. 2 class BaseFields < T::Struct
  14. 2 extend T::Sig
  15. 2 const :to, T.nilable(T::Array[String]), default: nil
  16. 2 const :from, T.nilable(String), default: nil
  17. 2 const :subject, T.nilable(String), default: nil
  18. 2 const :message_id, T.nilable(String), default: nil
  19. 2 const :mailer_class, T.nilable(String), default: nil
  20. 2 const :mailer_action, T.nilable(String), default: nil
  21. 2 const :attachment_count, T.nilable(Integer), default: nil
  22. 2 Kwargs = T.type_alias do
  23. {
  24. 1 to: T.nilable(T::Array[String]),
  25. from: T.nilable(String),
  26. subject: T.nilable(String),
  27. message_id: T.nilable(String),
  28. mailer_class: T.nilable(String),
  29. mailer_action: T.nilable(String),
  30. attachment_count: T.nilable(Integer)
  31. }
  32. end
  33. 3 sig { returns(Kwargs) }
  34. 2 def to_kwargs
  35. {
  36. 51 to: to,
  37. from: from,
  38. subject: subject,
  39. message_id: message_id,
  40. mailer_class: mailer_class,
  41. mailer_action: mailer_action,
  42. attachment_count: attachment_count
  43. }
  44. end
  45. end
  46. end
  47. end
  48. end

lib/log_struct/log/action_mailer/delivered.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActionMailer
  20. 2 class Delivered < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Mailer, default: Source::Mailer
  24. 2 const :event, Event, default: Event::Delivered
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :to, T.nilable(T::Array[String]), default: nil
  28. 2 const :from, T.nilable(String), default: nil
  29. 2 const :subject, T.nilable(String), default: nil
  30. 2 const :message_id, T.nilable(String), default: nil
  31. 2 const :mailer_class, T.nilable(String), default: nil
  32. 2 const :mailer_action, T.nilable(String), default: nil
  33. 2 const :attachment_count, T.nilable(Integer), default: nil
  34. # Additional data
  35. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  36. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  37. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  38. # Serialize shared fields
  39. 2 include LogStruct::Log::Interfaces::CommonFields
  40. 2 include LogStruct::Log::Shared::SerializeCommon
  41. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  42. 2 def to_h
  43. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 4 h[LogField::To] = to unless to.nil?
  45. 4 h[LogField::From] = from unless from.nil?
  46. 4 h[LogField::Subject] = subject unless subject.nil?
  47. 4 h[LogField::MessageId] = message_id unless message_id.nil?
  48. 4 h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
  49. 4 h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
  50. 4 h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
  51. 4 h
  52. end
  53. end
  54. end
  55. end
  56. end

lib/log_struct/log/action_mailer/delivery.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActionMailer
  20. 2 class Delivery < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Mailer, default: Source::Mailer
  24. 2 const :event, Event, default: Event::Delivery
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :to, T.nilable(T::Array[String]), default: nil
  28. 2 const :from, T.nilable(String), default: nil
  29. 2 const :subject, T.nilable(String), default: nil
  30. 2 const :message_id, T.nilable(String), default: nil
  31. 2 const :mailer_class, T.nilable(String), default: nil
  32. 2 const :mailer_action, T.nilable(String), default: nil
  33. 2 const :attachment_count, T.nilable(Integer), default: nil
  34. # Additional data
  35. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  36. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  37. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  38. # Serialize shared fields
  39. 2 include LogStruct::Log::Interfaces::CommonFields
  40. 2 include LogStruct::Log::Shared::SerializeCommon
  41. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  42. 2 def to_h
  43. 42 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 42 h[LogField::To] = to unless to.nil?
  45. 42 h[LogField::From] = from unless from.nil?
  46. 42 h[LogField::Subject] = subject unless subject.nil?
  47. 42 h[LogField::MessageId] = message_id unless message_id.nil?
  48. 42 h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
  49. 42 h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
  50. 42 h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
  51. 42 h
  52. end
  53. end
  54. end
  55. end
  56. end

lib/log_struct/log/action_mailer/error.rb

100.0% lines covered

48 relevant lines. 48 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActionMailer
  20. 2 class Error < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Mailer, default: Source::Mailer
  24. 2 const :event, Event, default: Event::Error
  25. 18 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :to, T.nilable(T::Array[String]), default: nil
  28. 2 const :from, T.nilable(String), default: nil
  29. 2 const :subject, T.nilable(String), default: nil
  30. 2 const :message_id, T.nilable(String), default: nil
  31. 2 const :mailer_class, T.nilable(String), default: nil
  32. 2 const :mailer_action, T.nilable(String), default: nil
  33. 2 const :attachment_count, T.nilable(Integer), default: nil
  34. # Event-specific fields
  35. 2 const :error_class, T.class_of(StandardError)
  36. 2 const :message, String
  37. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  38. # Additional data
  39. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  40. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  41. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  42. # Serialize shared fields
  43. 2 include LogStruct::Log::Interfaces::CommonFields
  44. 2 include LogStruct::Log::Shared::SerializeCommon
  45. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  46. 2 def to_h
  47. 54 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  48. 54 h[LogField::To] = to unless to.nil?
  49. 54 h[LogField::From] = from unless from.nil?
  50. 54 h[LogField::Subject] = subject unless subject.nil?
  51. 54 h[LogField::MessageId] = message_id unless message_id.nil?
  52. 54 h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
  53. 54 h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
  54. 54 h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
  55. 54 h[LogField::ErrorClass] = error_class
  56. 54 h[LogField::Message] = message
  57. 54 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  58. 54 h
  59. end
  60. end
  61. end
  62. end
  63. end

lib/log_struct/log/active_job.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "active_job/enqueue"
  8. 2 require_relative "active_job/schedule"
  9. 2 require_relative "active_job/start"
  10. 2 require_relative "active_job/finish"
  11. 2 require_relative "active_job/retry"
  12. 2 require_relative "active_job/retry_stopped"
  13. 2 require_relative "active_job/discard"
  14. 2 require_relative "active_job/error"
  15. 2 module LogStruct
  16. 2 module Log
  17. 2 class ActiveJob
  18. 2 class BaseFields < T::Struct
  19. 2 extend T::Sig
  20. 2 const :job_id, String
  21. 2 const :job_class, String
  22. 2 const :queue_name, T.nilable(Symbol), default: nil
  23. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  24. 2 const :executions, T.nilable(Integer), default: nil
  25. 2 const :provider_job_id, T.nilable(String), default: nil
  26. 2 Kwargs = T.type_alias do
  27. {
  28. 1 job_id: String,
  29. job_class: String,
  30. queue_name: T.nilable(Symbol),
  31. arguments: T.nilable(T::Array[T.untyped]),
  32. executions: T.nilable(Integer),
  33. provider_job_id: T.nilable(String)
  34. }
  35. end
  36. 3 sig { returns(Kwargs) }
  37. 2 def to_kwargs
  38. {
  39. 56 job_id: job_id,
  40. job_class: job_class,
  41. queue_name: queue_name,
  42. arguments: arguments,
  43. executions: executions,
  44. provider_job_id: provider_job_id
  45. }
  46. end
  47. end
  48. end
  49. end
  50. end

lib/log_struct/log/active_job/discard.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Discard < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Discard
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :error_class, T.class_of(Exception)
  35. 2 const :message, String
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 3 h[LogField::JobId] = job_id
  43. 3 h[LogField::JobClass] = job_class
  44. 3 h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. 3 h[LogField::Arguments] = arguments unless arguments.nil?
  46. 3 h[LogField::Executions] = executions unless executions.nil?
  47. 3 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  48. 3 h[LogField::ErrorClass] = error_class
  49. 3 h[LogField::Message] = message
  50. 3 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/active_job/enqueue.rb

100.0% lines covered

37 relevant lines. 37 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Enqueue < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Enqueue
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Serialize shared fields
  34. 2 include LogStruct::Log::Interfaces::CommonFields
  35. 2 include LogStruct::Log::Shared::SerializeCommon
  36. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  37. 2 def to_h
  38. 13 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  39. 13 h[LogField::JobId] = job_id
  40. 13 h[LogField::JobClass] = job_class
  41. 13 h[LogField::QueueName] = queue_name unless queue_name.nil?
  42. 13 h[LogField::Arguments] = arguments unless arguments.nil?
  43. 13 h[LogField::Executions] = executions unless executions.nil?
  44. 13 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  45. 13 h
  46. end
  47. end
  48. end
  49. end
  50. end

lib/log_struct/log/active_job/error.rb

100.0% lines covered

43 relevant lines. 43 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Error < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Error
  25. 16 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :error_class, T.class_of(Exception)
  35. 2 const :message, String
  36. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  37. # Serialize shared fields
  38. 2 include LogStruct::Log::Interfaces::CommonFields
  39. 2 include LogStruct::Log::Shared::SerializeCommon
  40. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  41. 2 def to_h
  42. 14 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  43. 14 h[LogField::JobId] = job_id
  44. 14 h[LogField::JobClass] = job_class
  45. 14 h[LogField::QueueName] = queue_name unless queue_name.nil?
  46. 14 h[LogField::Arguments] = arguments unless arguments.nil?
  47. 14 h[LogField::Executions] = executions unless executions.nil?
  48. 14 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  49. 14 h[LogField::ErrorClass] = error_class
  50. 14 h[LogField::Message] = message
  51. 14 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  52. 14 h
  53. end
  54. end
  55. end
  56. end
  57. end

lib/log_struct/log/active_job/finish.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Finish < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Finish
  25. 2 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :duration_ms, Float
  35. 2 const :finished_at, Time
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. 17 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 17 h[LogField::JobId] = job_id
  43. 17 h[LogField::JobClass] = job_class
  44. 17 h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. 17 h[LogField::Arguments] = arguments unless arguments.nil?
  46. 17 h[LogField::Executions] = executions unless executions.nil?
  47. 17 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  48. 17 h[LogField::DurationMs] = duration_ms
  49. 17 h[LogField::FinishedAt] = finished_at
  50. 17 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/active_job/retry.rb

100.0% lines covered

43 relevant lines. 43 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Retry < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Retry
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :error_class, T.nilable(T.class_of(Exception)), default: nil
  35. 2 const :message, T.nilable(String), default: nil
  36. 2 const :wait_ms, T.nilable(Float), default: nil
  37. # Serialize shared fields
  38. 2 include LogStruct::Log::Interfaces::CommonFields
  39. 2 include LogStruct::Log::Shared::SerializeCommon
  40. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  41. 2 def to_h
  42. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  43. 3 h[LogField::JobId] = job_id
  44. 3 h[LogField::JobClass] = job_class
  45. 3 h[LogField::QueueName] = queue_name unless queue_name.nil?
  46. 3 h[LogField::Arguments] = arguments unless arguments.nil?
  47. 3 h[LogField::Executions] = executions unless executions.nil?
  48. 3 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  49. 3 h[LogField::ErrorClass] = error_class unless error_class.nil?
  50. 3 h[LogField::Message] = message unless message.nil?
  51. 3 h[LogField::WaitMs] = wait_ms unless wait_ms.nil?
  52. 3 h
  53. end
  54. end
  55. end
  56. end
  57. end

lib/log_struct/log/active_job/retry_stopped.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class RetryStopped < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::RetryStopped
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :error_class, T.class_of(Exception)
  35. 2 const :message, String
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 3 h[LogField::JobId] = job_id
  43. 3 h[LogField::JobClass] = job_class
  44. 3 h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. 3 h[LogField::Arguments] = arguments unless arguments.nil?
  46. 3 h[LogField::Executions] = executions unless executions.nil?
  47. 3 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  48. 3 h[LogField::ErrorClass] = error_class
  49. 3 h[LogField::Message] = message
  50. 3 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/active_job/schedule.rb

100.0% lines covered

39 relevant lines. 39 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Schedule < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Schedule
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :scheduled_at, Time
  35. # Serialize shared fields
  36. 2 include LogStruct::Log::Interfaces::CommonFields
  37. 2 include LogStruct::Log::Shared::SerializeCommon
  38. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  39. 2 def to_h
  40. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  41. 3 h[LogField::JobId] = job_id
  42. 3 h[LogField::JobClass] = job_class
  43. 3 h[LogField::QueueName] = queue_name unless queue_name.nil?
  44. 3 h[LogField::Arguments] = arguments unless arguments.nil?
  45. 3 h[LogField::Executions] = executions unless executions.nil?
  46. 3 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  47. 3 h[LogField::ScheduledAt] = scheduled_at
  48. 3 h
  49. end
  50. end
  51. end
  52. end
  53. end

lib/log_struct/log/active_job/start.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveJob
  20. 2 class Start < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Start
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, String
  28. 2 const :job_class, String
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. 2 const :provider_job_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :started_at, Time
  35. 2 const :attempt, T.nilable(Integer), default: nil
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. 16 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 16 h[LogField::JobId] = job_id
  43. 16 h[LogField::JobClass] = job_class
  44. 16 h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. 16 h[LogField::Arguments] = arguments unless arguments.nil?
  46. 16 h[LogField::Executions] = executions unless executions.nil?
  47. 16 h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  48. 16 h[LogField::StartedAt] = started_at
  49. 16 h[LogField::Attempt] = attempt unless attempt.nil?
  50. 16 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/active_model_serializers.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../enums/source"
  14. 2 require_relative "../enums/event"
  15. 2 require_relative "../enums/level"
  16. 2 require_relative "../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveModelSerializers < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source::Rails, default: Source::Rails
  23. 2 const :event, Event, default: Event::Generate
  24. 4 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. # Event-specific fields
  27. 2 const :message, String
  28. 2 const :serializer, T.nilable(String), default: nil
  29. 2 const :adapter, T.nilable(String), default: nil
  30. 2 const :resource_class, T.nilable(String), default: nil
  31. 2 const :duration_ms, Float
  32. # Serialize shared fields
  33. 2 include LogStruct::Log::Interfaces::CommonFields
  34. 2 include LogStruct::Log::Shared::SerializeCommon
  35. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  36. 2 def to_h
  37. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  38. 3 h[LogField::Message] = message
  39. 3 h[LogField::Serializer] = serializer unless serializer.nil?
  40. 3 h[LogField::Adapter] = adapter unless adapter.nil?
  41. 3 h[LogField::ResourceClass] = resource_class unless resource_class.nil?
  42. 3 h[LogField::DurationMs] = duration_ms
  43. 3 h
  44. end
  45. end
  46. end
  47. end

lib/log_struct/log/active_storage.rb

88.89% lines covered

18 relevant lines. 16 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "active_storage/upload"
  8. 2 require_relative "active_storage/download"
  9. 2 require_relative "active_storage/delete"
  10. 2 require_relative "active_storage/metadata"
  11. 2 require_relative "active_storage/exist"
  12. 2 require_relative "active_storage/url"
  13. 2 module LogStruct
  14. 2 module Log
  15. 2 class ActiveStorage
  16. 2 class BaseFields < T::Struct
  17. 2 extend T::Sig
  18. 2 const :storage, Symbol
  19. 2 const :file_id, T.nilable(String), default: nil
  20. 2 Kwargs = T.type_alias do
  21. {
  22. storage: Symbol,
  23. file_id: T.nilable(String)
  24. }
  25. end
  26. 2 sig { returns(Kwargs) }
  27. 2 def to_kwargs
  28. {
  29. storage: storage,
  30. file_id: file_id
  31. }
  32. end
  33. end
  34. end
  35. end
  36. end

lib/log_struct/log/active_storage/delete.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveStorage
  20. 2 class Delete < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Storage, default: Source::Storage
  24. 2 const :event, Event, default: Event::Delete
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, T.nilable(String), default: nil
  29. # Event-specific fields
  30. 2 const :prefix, T.nilable(String), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. 2 h[LogField::Storage] = storage
  38. 2 h[LogField::FileId] = file_id unless file_id.nil?
  39. 2 h[LogField::Prefix] = prefix unless prefix.nil?
  40. 2 h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/active_storage/download.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveStorage
  20. 2 class Download < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Storage, default: Source::Storage
  24. 2 const :event, Event, default: Event::Download
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, T.nilable(String), default: nil
  29. # Event-specific fields
  30. 2 const :range, T.nilable(String), default: nil
  31. 2 const :duration_ms, T.nilable(Float), default: nil
  32. # Serialize shared fields
  33. 2 include LogStruct::Log::Interfaces::CommonFields
  34. 2 include LogStruct::Log::Shared::SerializeCommon
  35. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  36. 2 def to_h
  37. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  38. 2 h[LogField::Storage] = storage
  39. 2 h[LogField::FileId] = file_id unless file_id.nil?
  40. 2 h[LogField::Range] = range unless range.nil?
  41. 2 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  42. 2 h
  43. end
  44. end
  45. end
  46. end
  47. end

lib/log_struct/log/active_storage/exist.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveStorage
  20. 2 class Exist < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Storage, default: Source::Storage
  24. 2 const :event, Event, default: Event::Exist
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, T.nilable(String), default: nil
  29. # Event-specific fields
  30. 2 const :exist, T.nilable(T::Boolean), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. 2 h[LogField::Storage] = storage
  38. 2 h[LogField::FileId] = file_id unless file_id.nil?
  39. 2 h[LogField::Exist] = exist unless exist.nil?
  40. 2 h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/active_storage/metadata.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveStorage
  20. 2 class Metadata < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Storage, default: Source::Storage
  24. 2 const :event, Event, default: Event::Metadata
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, T.nilable(String), default: nil
  29. # Event-specific fields
  30. 2 const :mime_type, T.nilable(String), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. 2 h[LogField::Storage] = storage
  38. 2 h[LogField::FileId] = file_id unless file_id.nil?
  39. 2 h[LogField::MimeType] = mime_type unless mime_type.nil?
  40. 2 h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/active_storage/upload.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveStorage
  20. 2 class Upload < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Storage, default: Source::Storage
  24. 2 const :event, Event, default: Event::Upload
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, T.nilable(String), default: nil
  29. # Event-specific fields
  30. 2 const :duration_ms, T.nilable(Float), default: nil
  31. 2 const :checksum, T.nilable(String), default: nil
  32. # Serialize shared fields
  33. 2 include LogStruct::Log::Interfaces::CommonFields
  34. 2 include LogStruct::Log::Shared::SerializeCommon
  35. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  36. 2 def to_h
  37. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  38. 2 h[LogField::Storage] = storage
  39. 2 h[LogField::FileId] = file_id unless file_id.nil?
  40. 2 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  41. 2 h[LogField::Checksum] = checksum unless checksum.nil?
  42. 2 h
  43. end
  44. end
  45. end
  46. end
  47. end

lib/log_struct/log/active_storage/url.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class ActiveStorage
  20. 2 class Url < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Storage, default: Source::Storage
  24. 2 const :event, Event, default: Event::Url
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, T.nilable(String), default: nil
  29. # Serialize shared fields
  30. 2 include LogStruct::Log::Interfaces::CommonFields
  31. 2 include LogStruct::Log::Shared::SerializeCommon
  32. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. 2 h[LogField::Storage] = storage
  36. 2 h[LogField::FileId] = file_id unless file_id.nil?
  37. 2 h
  38. end
  39. end
  40. end
  41. end
  42. end

lib/log_struct/log/ahoy.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../enums/source"
  14. 2 require_relative "../enums/event"
  15. 2 require_relative "../enums/level"
  16. 2 require_relative "../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Ahoy < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source::App, default: Source::App
  23. 2 const :event, Event, default: Event::Log
  24. 8 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. # Event-specific fields
  27. 2 const :message, String
  28. 2 const :ahoy_event, String
  29. 2 const :properties, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  30. # Serialize shared fields
  31. 2 include LogStruct::Log::Interfaces::CommonFields
  32. 2 include LogStruct::Log::Shared::SerializeCommon
  33. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  34. 2 def to_h
  35. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  36. 4 h[LogField::Message] = message
  37. 4 h[LogField::AhoyEvent] = ahoy_event
  38. 4 h[LogField::Properties] = properties unless properties.nil?
  39. 4 h
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/carrierwave.rb

90.48% lines covered

21 relevant lines. 19 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "carrierwave/upload"
  8. 2 require_relative "carrierwave/delete"
  9. 2 require_relative "carrierwave/download"
  10. 2 module LogStruct
  11. 2 module Log
  12. 2 class CarrierWave
  13. 2 class BaseFields < T::Struct
  14. 2 extend T::Sig
  15. 2 const :storage, Symbol
  16. 2 const :file_id, String
  17. 2 const :uploader, T.nilable(String), default: nil
  18. 2 const :model, T.nilable(String), default: nil
  19. 2 const :mount_point, T.nilable(String), default: nil
  20. 2 const :version, T.nilable(String), default: nil
  21. 2 const :store_path, T.nilable(String), default: nil
  22. 2 const :extension, T.nilable(String), default: nil
  23. 2 Kwargs = T.type_alias do
  24. {
  25. storage: Symbol,
  26. file_id: String,
  27. uploader: T.nilable(String),
  28. model: T.nilable(String),
  29. mount_point: T.nilable(String),
  30. version: T.nilable(String),
  31. store_path: T.nilable(String),
  32. extension: T.nilable(String)
  33. }
  34. end
  35. 2 sig { returns(Kwargs) }
  36. 2 def to_kwargs
  37. {
  38. storage: storage,
  39. file_id: file_id,
  40. uploader: uploader,
  41. model: model,
  42. mount_point: mount_point,
  43. version: version,
  44. store_path: store_path,
  45. extension: extension
  46. }
  47. end
  48. end
  49. end
  50. end
  51. end

lib/log_struct/log/carrierwave/delete.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class CarrierWave
  20. 2 class Delete < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::CarrierWave, default: Source::CarrierWave
  24. 2 const :event, Event, default: Event::Delete
  25. 5 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, String
  29. 2 const :uploader, T.nilable(String), default: nil
  30. 2 const :model, T.nilable(String), default: nil
  31. 2 const :mount_point, T.nilable(String), default: nil
  32. 2 const :version, T.nilable(String), default: nil
  33. 2 const :store_path, T.nilable(String), default: nil
  34. 2 const :extension, T.nilable(String), default: nil
  35. # Serialize shared fields
  36. 2 include LogStruct::Log::Interfaces::CommonFields
  37. 2 include LogStruct::Log::Shared::SerializeCommon
  38. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  39. 2 def to_h
  40. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  41. 3 h[LogField::Storage] = storage
  42. 3 h[LogField::FileId] = file_id
  43. 3 h[LogField::Uploader] = uploader unless uploader.nil?
  44. 3 h[LogField::Model] = model unless model.nil?
  45. 3 h[LogField::MountPoint] = mount_point unless mount_point.nil?
  46. 3 h[LogField::Version] = version unless version.nil?
  47. 3 h[LogField::StorePath] = store_path unless store_path.nil?
  48. 3 h[LogField::Extension] = extension unless extension.nil?
  49. 3 h
  50. end
  51. end
  52. end
  53. end
  54. end

lib/log_struct/log/carrierwave/download.rb

100.0% lines covered

47 relevant lines. 47 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class CarrierWave
  20. 2 class Download < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::CarrierWave, default: Source::CarrierWave
  24. 2 const :event, Event, default: Event::Download
  25. 5 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, String
  29. 2 const :uploader, T.nilable(String), default: nil
  30. 2 const :model, T.nilable(String), default: nil
  31. 2 const :mount_point, T.nilable(String), default: nil
  32. 2 const :version, T.nilable(String), default: nil
  33. 2 const :store_path, T.nilable(String), default: nil
  34. 2 const :extension, T.nilable(String), default: nil
  35. # Event-specific fields
  36. 2 const :filename, T.nilable(String), default: nil
  37. 2 const :mime_type, T.nilable(String), default: nil
  38. 2 const :size, T.nilable(Integer), default: nil
  39. # Serialize shared fields
  40. 2 include LogStruct::Log::Interfaces::CommonFields
  41. 2 include LogStruct::Log::Shared::SerializeCommon
  42. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  43. 2 def to_h
  44. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  45. 3 h[LogField::Storage] = storage
  46. 3 h[LogField::FileId] = file_id
  47. 3 h[LogField::Uploader] = uploader unless uploader.nil?
  48. 3 h[LogField::Model] = model unless model.nil?
  49. 3 h[LogField::MountPoint] = mount_point unless mount_point.nil?
  50. 3 h[LogField::Version] = version unless version.nil?
  51. 3 h[LogField::StorePath] = store_path unless store_path.nil?
  52. 3 h[LogField::Extension] = extension unless extension.nil?
  53. 3 h[LogField::Filename] = filename unless filename.nil?
  54. 3 h[LogField::MimeType] = mime_type unless mime_type.nil?
  55. 3 h[LogField::Size] = size unless size.nil?
  56. 3 h
  57. end
  58. end
  59. end
  60. end
  61. end

lib/log_struct/log/carrierwave/upload.rb

100.0% lines covered

51 relevant lines. 51 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class CarrierWave
  20. 2 class Upload < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::CarrierWave, default: Source::CarrierWave
  24. 2 const :event, Event, default: Event::Upload
  25. 8 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :storage, Symbol
  28. 2 const :file_id, String
  29. 2 const :uploader, T.nilable(String), default: nil
  30. 2 const :model, T.nilable(String), default: nil
  31. 2 const :mount_point, T.nilable(String), default: nil
  32. 2 const :version, T.nilable(String), default: nil
  33. 2 const :store_path, T.nilable(String), default: nil
  34. 2 const :extension, T.nilable(String), default: nil
  35. # Event-specific fields
  36. 2 const :filename, T.nilable(String), default: nil
  37. 2 const :mime_type, T.nilable(String), default: nil
  38. 2 const :size, T.nilable(Integer), default: nil
  39. 2 const :metadata, T.nilable(T::Hash[String, T.untyped]), default: nil
  40. 2 const :duration_ms, T.nilable(Float), default: nil
  41. # Serialize shared fields
  42. 2 include LogStruct::Log::Interfaces::CommonFields
  43. 2 include LogStruct::Log::Shared::SerializeCommon
  44. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  45. 2 def to_h
  46. 6 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  47. 6 h[LogField::Storage] = storage
  48. 6 h[LogField::FileId] = file_id
  49. 6 h[LogField::Uploader] = uploader unless uploader.nil?
  50. 6 h[LogField::Model] = model unless model.nil?
  51. 6 h[LogField::MountPoint] = mount_point unless mount_point.nil?
  52. 6 h[LogField::Version] = version unless version.nil?
  53. 6 h[LogField::StorePath] = store_path unless store_path.nil?
  54. 6 h[LogField::Extension] = extension unless extension.nil?
  55. 6 h[LogField::Filename] = filename unless filename.nil?
  56. 6 h[LogField::MimeType] = mime_type unless mime_type.nil?
  57. 6 h[LogField::Size] = size unless size.nil?
  58. 6 h[LogField::Metadata] = metadata unless metadata.nil?
  59. 6 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  60. 6 h
  61. end
  62. end
  63. end
  64. end
  65. end

lib/log_struct/log/dotenv.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "dotenv/load"
  8. 2 require_relative "dotenv/update"
  9. 2 require_relative "dotenv/save"
  10. 2 require_relative "dotenv/restore"

lib/log_struct/log/dotenv/load.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Dotenv
  20. 2 class Load < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Dotenv, default: Source::Dotenv
  24. 2 const :event, Event, default: Event::Load
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :file, String
  29. # Serialize shared fields
  30. 2 include LogStruct::Log::Interfaces::CommonFields
  31. 2 include LogStruct::Log::Shared::SerializeCommon
  32. 4 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. 4 h[LogField::File] = file
  36. 4 h
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/dotenv/restore.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Dotenv
  20. 2 class Restore < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Dotenv, default: Source::Dotenv
  24. 2 const :event, Event, default: Event::Restore
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :vars, T::Array[String]
  29. # Serialize shared fields
  30. 2 include LogStruct::Log::Interfaces::CommonFields
  31. 2 include LogStruct::Log::Shared::SerializeCommon
  32. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. 2 h[LogField::Vars] = vars
  36. 2 h
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/dotenv/save.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Dotenv
  20. 2 class Save < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Dotenv, default: Source::Dotenv
  24. 2 const :event, Event, default: Event::Save
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :snapshot, T::Boolean
  29. # Serialize shared fields
  30. 2 include LogStruct::Log::Interfaces::CommonFields
  31. 2 include LogStruct::Log::Shared::SerializeCommon
  32. 4 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. 4 h[LogField::Snapshot] = snapshot
  36. 4 h
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/dotenv/update.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Dotenv
  20. 2 class Update < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Dotenv, default: Source::Dotenv
  24. 2 const :event, Event, default: Event::Update
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :vars, T::Array[String]
  29. # Serialize shared fields
  30. 2 include LogStruct::Log::Interfaces::CommonFields
  31. 2 include LogStruct::Log::Shared::SerializeCommon
  32. 4 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. 4 h[LogField::Vars] = vars
  36. 4 h
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/error.rb

100.0% lines covered

53 relevant lines. 53 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../enums/source"
  14. 2 require_relative "../enums/event"
  15. 2 require_relative "../enums/level"
  16. 2 require_relative "../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Error < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source
  23. 2 const :event, Event, default: Event::Error
  24. 20 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. 2 const :path, T.nilable(String), default: nil
  27. 2 const :http_method, T.nilable(String), default: nil
  28. 2 const :source_ip, T.nilable(String), default: nil
  29. 2 const :user_agent, T.nilable(String), default: nil
  30. 2 const :referer, T.nilable(String), default: nil
  31. # Event-specific fields
  32. 2 const :error_class, T.class_of(Exception)
  33. 2 const :message, String
  34. 2 const :error_message, T.nilable(String), default: nil
  35. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  36. 2 const :status, T.nilable(Integer), default: nil
  37. 2 const :cause_class, T.nilable(String), default: nil
  38. 2 const :cause_message, T.nilable(String), default: nil
  39. # Additional data
  40. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  41. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  42. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  43. # Request fields (optional)
  44. 2 include LogStruct::Log::Interfaces::RequestFields
  45. # Serialize shared fields
  46. 2 include LogStruct::Log::Interfaces::CommonFields
  47. 2 include LogStruct::Log::Shared::SerializeCommon
  48. 2 include LogStruct::Log::Shared::AddRequestFields
  49. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  50. 2 def to_h
  51. 36 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  52. 36 h[LogField::Path] = path unless path.nil?
  53. 36 h[LogField::HttpMethod] = http_method unless http_method.nil?
  54. 36 h[LogField::SourceIp] = source_ip unless source_ip.nil?
  55. 36 h[LogField::UserAgent] = user_agent unless user_agent.nil?
  56. 36 h[LogField::Referer] = referer unless referer.nil?
  57. 36 h[LogField::ErrorClass] = error_class
  58. 36 h[LogField::Message] = message
  59. 36 h[LogField::ErrorMessage] = error_message unless error_message.nil?
  60. 36 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  61. 36 h[LogField::Status] = status unless status.nil?
  62. 36 h[LogField::CauseClass] = cause_class unless cause_class.nil?
  63. 36 h[LogField::CauseMessage] = cause_message unless cause_message.nil?
  64. 36 h
  65. end
  66. end
  67. end
  68. end

lib/log_struct/log/good_job.rb

88.24% lines covered

17 relevant lines. 15 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "good_job/log"
  8. 2 require_relative "good_job/error"
  9. 2 module LogStruct
  10. 2 module Log
  11. 2 class GoodJob
  12. 2 class BaseFields < T::Struct
  13. 2 extend T::Sig
  14. 2 const :job_id, T.nilable(String), default: nil
  15. 2 const :job_class, T.nilable(String), default: nil
  16. 2 const :queue_name, T.nilable(Symbol), default: nil
  17. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  18. 2 const :executions, T.nilable(Integer), default: nil
  19. 2 Kwargs = T.type_alias do
  20. {
  21. job_id: T.nilable(String),
  22. job_class: T.nilable(String),
  23. queue_name: T.nilable(Symbol),
  24. arguments: T.nilable(T::Array[T.untyped]),
  25. executions: T.nilable(Integer)
  26. }
  27. end
  28. 2 sig { returns(Kwargs) }
  29. 2 def to_kwargs
  30. {
  31. job_id: job_id,
  32. job_class: job_class,
  33. queue_name: queue_name,
  34. arguments: arguments,
  35. executions: executions
  36. }
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/good_job/error.rb

100.0% lines covered

49 relevant lines. 49 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class GoodJob
  20. 2 class Error < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Error
  25. 12 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, T.nilable(String), default: nil
  28. 2 const :job_class, T.nilable(String), default: nil
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. # Event-specific fields
  33. 2 const :error_class, String
  34. 2 const :error_message, String
  35. 2 const :duration_ms, T.nilable(Float), default: nil
  36. 2 const :process_id, Integer
  37. 2 const :thread_id, String
  38. 2 const :exception_executions, T.nilable(Integer), default: nil
  39. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  40. # Serialize shared fields
  41. 2 include LogStruct::Log::Interfaces::CommonFields
  42. 2 include LogStruct::Log::Shared::SerializeCommon
  43. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  44. 2 def to_h
  45. 5 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  46. 5 h[LogField::JobId] = job_id unless job_id.nil?
  47. 5 h[LogField::JobClass] = job_class unless job_class.nil?
  48. 5 h[LogField::QueueName] = queue_name unless queue_name.nil?
  49. 5 h[LogField::Arguments] = arguments unless arguments.nil?
  50. 5 h[LogField::Executions] = executions unless executions.nil?
  51. 5 h[LogField::ErrorClass] = error_class
  52. 5 h[LogField::ErrorMessage] = error_message
  53. 5 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  54. 5 h[LogField::ProcessId] = process_id
  55. 5 h[LogField::ThreadId] = thread_id
  56. 5 h[LogField::ExceptionExecutions] = exception_executions unless exception_executions.nil?
  57. 5 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  58. 5 h
  59. end
  60. end
  61. end
  62. end
  63. end

lib/log_struct/log/good_job/log.rb

100.0% lines covered

45 relevant lines. 45 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class GoodJob
  20. 2 class Log < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Job, default: Source::Job
  24. 2 const :event, Event, default: Event::Log
  25. 24 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :job_id, T.nilable(String), default: nil
  28. 2 const :job_class, T.nilable(String), default: nil
  29. 2 const :queue_name, T.nilable(Symbol), default: nil
  30. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  31. 2 const :executions, T.nilable(Integer), default: nil
  32. # Event-specific fields
  33. 2 const :message, String
  34. 2 const :process_id, Integer
  35. 2 const :thread_id, String
  36. 2 const :scheduled_at, T.nilable(Time), default: nil
  37. 2 const :priority, T.nilable(Integer), default: nil
  38. # Serialize shared fields
  39. 2 include LogStruct::Log::Interfaces::CommonFields
  40. 2 include LogStruct::Log::Shared::SerializeCommon
  41. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  42. 2 def to_h
  43. 17 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 17 h[LogField::JobId] = job_id unless job_id.nil?
  45. 17 h[LogField::JobClass] = job_class unless job_class.nil?
  46. 17 h[LogField::QueueName] = queue_name unless queue_name.nil?
  47. 17 h[LogField::Arguments] = arguments unless arguments.nil?
  48. 17 h[LogField::Executions] = executions unless executions.nil?
  49. 17 h[LogField::Message] = message
  50. 17 h[LogField::ProcessId] = process_id
  51. 17 h[LogField::ThreadId] = thread_id
  52. 17 h[LogField::ScheduledAt] = scheduled_at unless scheduled_at.nil?
  53. 17 h[LogField::Priority] = priority unless priority.nil?
  54. 17 h
  55. end
  56. end
  57. end
  58. end
  59. end

lib/log_struct/log/interfaces/public_common_fields.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "log_struct/shared/interfaces/public_common_fields"

lib/log_struct/log/plain.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../enums/source"
  14. 2 require_relative "../enums/event"
  15. 2 require_relative "../enums/level"
  16. 2 require_relative "../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Plain < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source::App, default: Source::App
  23. 2 const :event, Event, default: Event::Log
  24. 27 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. # Event-specific fields
  27. 2 const :message, T.untyped
  28. # Additional data
  29. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  30. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  31. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  32. # Serialize shared fields
  33. 2 include LogStruct::Log::Interfaces::CommonFields
  34. 2 include LogStruct::Log::Shared::SerializeCommon
  35. 4 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  36. 2 def to_h
  37. 1385 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  38. 1385 h[LogField::Message] = message
  39. 1385 h
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/puma.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "puma/start"
  8. 2 require_relative "puma/shutdown"
  9. 2 require_relative "puma/error"

lib/log_struct/log/puma/error.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Puma
  20. 2 class Error < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Puma, default: Source::Puma
  24. 2 const :event, Event, default: Event::Error
  25. 6 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :error_class, T.class_of(Exception)
  29. 2 const :message, String
  30. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  31. 2 const :error_context, T.nilable(String), default: nil
  32. 2 const :http_method, T.nilable(String), default: nil
  33. 2 const :path, T.nilable(String), default: nil
  34. # Additional data
  35. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  36. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  37. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  38. # Serialize shared fields
  39. 2 include LogStruct::Log::Interfaces::CommonFields
  40. 2 include LogStruct::Log::Shared::SerializeCommon
  41. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  42. 2 def to_h
  43. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 4 h[LogField::ErrorClass] = error_class
  45. 4 h[LogField::Message] = message
  46. 4 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  47. 4 h[LogField::ErrorContext] = error_context unless error_context.nil?
  48. 4 h[LogField::HttpMethod] = http_method unless http_method.nil?
  49. 4 h[LogField::Path] = path unless path.nil?
  50. 4 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/puma/shutdown.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Puma
  20. 2 class Shutdown < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Puma, default: Source::Puma
  24. 2 const :event, Event, default: Event::Shutdown
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :process_id, T.nilable(Integer), default: nil
  29. # Additional data
  30. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  31. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  32. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  33. # Serialize shared fields
  34. 2 include LogStruct::Log::Interfaces::CommonFields
  35. 2 include LogStruct::Log::Shared::SerializeCommon
  36. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  37. 2 def to_h
  38. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  39. 2 h[LogField::ProcessId] = process_id unless process_id.nil?
  40. 2 h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/puma/start.rb

100.0% lines covered

46 relevant lines. 46 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Puma
  20. 2 class Start < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Puma, default: Source::Puma
  24. 2 const :event, Event, default: Event::Start
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :mode, T.nilable(String), default: nil
  29. 2 const :puma_version, T.nilable(String), default: nil
  30. 2 const :puma_codename, T.nilable(String), default: nil
  31. 2 const :ruby_version, T.nilable(String), default: nil
  32. 2 const :min_threads, T.nilable(Integer), default: nil
  33. 2 const :max_threads, T.nilable(Integer), default: nil
  34. 2 const :environment, T.nilable(String), default: nil
  35. 2 const :process_id, T.nilable(Integer), default: nil
  36. 2 const :listening_addresses, T.nilable(T::Array[String]), default: nil
  37. # Additional data
  38. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  39. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  40. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  41. # Serialize shared fields
  42. 2 include LogStruct::Log::Interfaces::CommonFields
  43. 2 include LogStruct::Log::Shared::SerializeCommon
  44. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  45. 2 def to_h
  46. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  47. 2 h[LogField::Mode] = mode unless mode.nil?
  48. 2 h[LogField::PumaVersion] = puma_version unless puma_version.nil?
  49. 2 h[LogField::PumaCodename] = puma_codename unless puma_codename.nil?
  50. 2 h[LogField::RubyVersion] = ruby_version unless ruby_version.nil?
  51. 2 h[LogField::MinThreads] = min_threads unless min_threads.nil?
  52. 2 h[LogField::MaxThreads] = max_threads unless max_threads.nil?
  53. 2 h[LogField::Environment] = environment unless environment.nil?
  54. 2 h[LogField::ProcessId] = process_id unless process_id.nil?
  55. 2 h[LogField::ListeningAddresses] = listening_addresses unless listening_addresses.nil?
  56. 2 h
  57. end
  58. end
  59. end
  60. end
  61. end

lib/log_struct/log/request.rb

100.0% lines covered

61 relevant lines. 61 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../enums/source"
  14. 2 require_relative "../enums/event"
  15. 2 require_relative "../enums/level"
  16. 2 require_relative "../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Request < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source::Rails, default: Source::Rails
  23. 2 const :event, Event, default: Event::Request
  24. 7 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. 2 const :path, T.nilable(String), default: nil
  27. 2 const :http_method, T.nilable(String), default: nil
  28. 2 const :source_ip, T.nilable(String), default: nil
  29. 2 const :user_agent, T.nilable(String), default: nil
  30. 2 const :referer, T.nilable(String), default: nil
  31. # Event-specific fields
  32. 2 const :format, T.nilable(Symbol), default: nil
  33. 2 const :controller, T.nilable(String), default: nil
  34. 2 const :action, T.nilable(String), default: nil
  35. 2 const :status, T.nilable(Integer), default: nil
  36. 2 const :duration_ms, T.nilable(Float), default: nil
  37. 2 const :view, T.nilable(Float), default: nil
  38. 2 const :database, T.nilable(Float), default: nil
  39. 2 const :params, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  40. 2 const :host, T.nilable(String), default: nil
  41. 2 const :content_type, T.nilable(String), default: nil
  42. 2 const :accept, T.nilable(String), default: nil
  43. # Additional data
  44. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  45. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  46. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  47. # Request fields (optional)
  48. 2 include LogStruct::Log::Interfaces::RequestFields
  49. # Serialize shared fields
  50. 2 include LogStruct::Log::Interfaces::CommonFields
  51. 2 include LogStruct::Log::Shared::SerializeCommon
  52. 2 include LogStruct::Log::Shared::AddRequestFields
  53. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  54. 2 def to_h
  55. 8 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  56. 8 h[LogField::Path] = path unless path.nil?
  57. 8 h[LogField::HttpMethod] = http_method unless http_method.nil?
  58. 8 h[LogField::SourceIp] = source_ip unless source_ip.nil?
  59. 8 h[LogField::UserAgent] = user_agent unless user_agent.nil?
  60. 8 h[LogField::Referer] = referer unless referer.nil?
  61. 8 h[LogField::Format] = format unless format.nil?
  62. 8 h[LogField::Controller] = controller unless controller.nil?
  63. 8 h[LogField::Action] = action unless action.nil?
  64. 8 h[LogField::Status] = status unless status.nil?
  65. 8 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  66. 8 h[LogField::View] = view unless view.nil?
  67. 8 h[LogField::Database] = database unless database.nil?
  68. 8 h[LogField::Params] = params unless params.nil?
  69. 8 h[LogField::Host] = host unless host.nil?
  70. 8 h[LogField::ContentType] = content_type unless content_type.nil?
  71. 8 h[LogField::Accept] = accept unless accept.nil?
  72. 8 h
  73. end
  74. end
  75. end
  76. end

lib/log_struct/log/security.rb

88.89% lines covered

18 relevant lines. 16 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "security/ip_spoof"
  8. 2 require_relative "security/csrf_violation"
  9. 2 require_relative "security/blocked_host"
  10. 2 module LogStruct
  11. 2 module Log
  12. 2 class Security
  13. 2 class BaseFields < T::Struct
  14. 2 extend T::Sig
  15. 2 const :path, T.nilable(String), default: nil
  16. 2 const :http_method, T.nilable(String), default: nil
  17. 2 const :source_ip, T.nilable(String), default: nil
  18. 2 const :user_agent, T.nilable(String), default: nil
  19. 2 const :referer, T.nilable(String), default: nil
  20. 2 Kwargs = T.type_alias do
  21. {
  22. path: T.nilable(String),
  23. http_method: T.nilable(String),
  24. source_ip: T.nilable(String),
  25. user_agent: T.nilable(String),
  26. referer: T.nilable(String)
  27. }
  28. end
  29. 2 sig { returns(Kwargs) }
  30. 2 def to_kwargs
  31. {
  32. path: path,
  33. http_method: http_method,
  34. source_ip: source_ip,
  35. user_agent: user_agent,
  36. referer: referer
  37. }
  38. end
  39. end
  40. end
  41. end
  42. end

lib/log_struct/log/security/blocked_host.rb

100.0% lines covered

48 relevant lines. 48 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Security
  20. 2 class BlockedHost < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Security, default: Source::Security
  24. 2 const :event, Event, default: Event::BlockedHost
  25. 8 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :path, T.nilable(String), default: nil
  28. 2 const :http_method, T.nilable(String), default: nil
  29. 2 const :source_ip, T.nilable(String), default: nil
  30. 2 const :user_agent, T.nilable(String), default: nil
  31. 2 const :referer, T.nilable(String), default: nil
  32. # Event-specific fields
  33. 2 const :message, T.nilable(String), default: nil
  34. 2 const :blocked_host, T.nilable(String), default: nil
  35. 2 const :blocked_hosts, T.nilable(T::Array[String]), default: nil
  36. 2 const :x_forwarded_for, T.nilable(String), default: nil
  37. # Additional data
  38. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  39. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  40. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  41. # Request fields (optional)
  42. 2 include LogStruct::Log::Interfaces::RequestFields
  43. # Serialize shared fields
  44. 2 include LogStruct::Log::Interfaces::CommonFields
  45. 2 include LogStruct::Log::Shared::SerializeCommon
  46. 2 include LogStruct::Log::Shared::AddRequestFields
  47. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  48. 2 def to_h
  49. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  50. 3 h[LogField::Path] = path unless path.nil?
  51. 3 h[LogField::HttpMethod] = http_method unless http_method.nil?
  52. 3 h[LogField::SourceIp] = source_ip unless source_ip.nil?
  53. 3 h[LogField::UserAgent] = user_agent unless user_agent.nil?
  54. 3 h[LogField::Referer] = referer unless referer.nil?
  55. 3 h[LogField::Message] = message unless message.nil?
  56. 3 h[LogField::BlockedHost] = blocked_host unless blocked_host.nil?
  57. 3 h[LogField::BlockedHosts] = blocked_hosts unless blocked_hosts.nil?
  58. 3 h[LogField::XForwardedFor] = x_forwarded_for unless x_forwarded_for.nil?
  59. 3 h
  60. end
  61. end
  62. end
  63. end
  64. end

lib/log_struct/log/security/csrf_violation.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Security
  20. 2 class CSRFViolation < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Security, default: Source::Security
  24. 2 const :event, Event, default: Event::CSRFViolation
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :path, T.nilable(String), default: nil
  28. 2 const :http_method, T.nilable(String), default: nil
  29. 2 const :source_ip, T.nilable(String), default: nil
  30. 2 const :user_agent, T.nilable(String), default: nil
  31. 2 const :referer, T.nilable(String), default: nil
  32. # Event-specific fields
  33. 2 const :message, T.nilable(String), default: nil
  34. # Additional data
  35. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  36. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  37. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  38. # Request fields (optional)
  39. 2 include LogStruct::Log::Interfaces::RequestFields
  40. # Serialize shared fields
  41. 2 include LogStruct::Log::Interfaces::CommonFields
  42. 2 include LogStruct::Log::Shared::SerializeCommon
  43. 2 include LogStruct::Log::Shared::AddRequestFields
  44. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  45. 2 def to_h
  46. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  47. 4 h[LogField::Path] = path unless path.nil?
  48. 4 h[LogField::HttpMethod] = http_method unless http_method.nil?
  49. 4 h[LogField::SourceIp] = source_ip unless source_ip.nil?
  50. 4 h[LogField::UserAgent] = user_agent unless user_agent.nil?
  51. 4 h[LogField::Referer] = referer unless referer.nil?
  52. 4 h[LogField::Message] = message unless message.nil?
  53. 4 h
  54. end
  55. end
  56. end
  57. end
  58. end

lib/log_struct/log/security/ip_spoof.rb

100.0% lines covered

46 relevant lines. 46 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Security
  20. 2 class IPSpoof < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Security, default: Source::Security
  24. 2 const :event, Event, default: Event::IPSpoof
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. 2 const :path, T.nilable(String), default: nil
  28. 2 const :http_method, T.nilable(String), default: nil
  29. 2 const :source_ip, T.nilable(String), default: nil
  30. 2 const :user_agent, T.nilable(String), default: nil
  31. 2 const :referer, T.nilable(String), default: nil
  32. # Event-specific fields
  33. 2 const :message, T.nilable(String), default: nil
  34. 2 const :client_ip, T.nilable(String), default: nil
  35. 2 const :x_forwarded_for, T.nilable(String), default: nil
  36. # Additional data
  37. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  38. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  39. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  40. # Request fields (optional)
  41. 2 include LogStruct::Log::Interfaces::RequestFields
  42. # Serialize shared fields
  43. 2 include LogStruct::Log::Interfaces::CommonFields
  44. 2 include LogStruct::Log::Shared::SerializeCommon
  45. 2 include LogStruct::Log::Shared::AddRequestFields
  46. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  47. 2 def to_h
  48. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  49. 2 h[LogField::Path] = path unless path.nil?
  50. 2 h[LogField::HttpMethod] = http_method unless http_method.nil?
  51. 2 h[LogField::SourceIp] = source_ip unless source_ip.nil?
  52. 2 h[LogField::UserAgent] = user_agent unless user_agent.nil?
  53. 2 h[LogField::Referer] = referer unless referer.nil?
  54. 2 h[LogField::Message] = message unless message.nil?
  55. 2 h[LogField::ClientIp] = client_ip unless client_ip.nil?
  56. 2 h[LogField::XForwardedFor] = x_forwarded_for unless x_forwarded_for.nil?
  57. 2 h
  58. end
  59. end
  60. end
  61. end
  62. end

lib/log_struct/log/shrine.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "shrine/upload"
  8. 2 require_relative "shrine/download"
  9. 2 require_relative "shrine/delete"
  10. 2 require_relative "shrine/metadata"
  11. 2 require_relative "shrine/exist"

lib/log_struct/log/shrine/delete.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Shrine
  20. 2 class Delete < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Shrine, default: Source::Shrine
  24. 2 const :event, Event, default: Event::Delete
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :storage, Symbol
  29. 2 const :location, String
  30. # Serialize shared fields
  31. 2 include LogStruct::Log::Interfaces::CommonFields
  32. 2 include LogStruct::Log::Shared::SerializeCommon
  33. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  34. 2 def to_h
  35. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  36. 2 h[LogField::Storage] = storage
  37. 2 h[LogField::Location] = location
  38. 2 h
  39. end
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/shrine/download.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Shrine
  20. 2 class Download < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Shrine, default: Source::Shrine
  24. 2 const :event, Event, default: Event::Download
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :storage, Symbol
  29. 2 const :location, String
  30. 2 const :download_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. 2 h[LogField::Storage] = storage
  38. 2 h[LogField::Location] = location
  39. 2 h[LogField::DownloadOptions] = download_options unless download_options.nil?
  40. 2 h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/shrine/exist.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Shrine
  20. 2 class Exist < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Shrine, default: Source::Shrine
  24. 2 const :event, Event, default: Event::Exist
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :storage, Symbol
  29. 2 const :location, String
  30. # Serialize shared fields
  31. 2 include LogStruct::Log::Interfaces::CommonFields
  32. 2 include LogStruct::Log::Shared::SerializeCommon
  33. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  34. 2 def to_h
  35. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  36. 2 h[LogField::Storage] = storage
  37. 2 h[LogField::Location] = location
  38. 2 h
  39. end
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/shrine/metadata.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Shrine
  20. 2 class Metadata < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Shrine, default: Source::Shrine
  24. 2 const :event, Event, default: Event::Metadata
  25. 4 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :storage, Symbol
  29. 2 const :duration_ms, T.nilable(Float), default: nil
  30. # Serialize shared fields
  31. 2 include LogStruct::Log::Interfaces::CommonFields
  32. 2 include LogStruct::Log::Shared::SerializeCommon
  33. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  34. 2 def to_h
  35. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  36. 2 h[LogField::Storage] = storage
  37. 2 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  38. 2 h
  39. end
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/shrine/upload.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Shrine
  20. 2 class Upload < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Shrine, default: Source::Shrine
  24. 2 const :event, Event, default: Event::Upload
  25. 5 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :storage, Symbol
  29. 2 const :location, String
  30. 2 const :mime_type, T.nilable(String), default: nil
  31. 2 const :size, T.nilable(Integer), default: nil
  32. 2 const :upload_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  33. 2 const :options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  34. 2 const :uploader, T.nilable(String), default: nil
  35. 2 const :duration_ms, T.nilable(Float), default: nil
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 3 h[LogField::Storage] = storage
  43. 3 h[LogField::Location] = location
  44. 3 h[LogField::MimeType] = mime_type unless mime_type.nil?
  45. 3 h[LogField::Size] = size unless size.nil?
  46. 3 h[LogField::UploadOptions] = upload_options unless upload_options.nil?
  47. 3 h[LogField::Options] = options unless options.nil?
  48. 3 h[LogField::Uploader] = uploader unless uploader.nil?
  49. 3 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  50. 3 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/sidekiq.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/source_parent.rb.erb
  7. 2 require_relative "sidekiq/log"
  8. 2 require_relative "sidekiq/error"

lib/log_struct/log/sidekiq/error.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Sidekiq
  20. 2 class Error < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Sidekiq, default: Source::Sidekiq
  24. 2 const :event, Event, default: Event::Error
  25. 6 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :error_class, T.class_of(Exception)
  29. 2 const :message, String
  30. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  31. 2 const :error_context, T.nilable(String), default: nil
  32. 2 const :queue_name, T.nilable(String), default: nil
  33. 2 const :retry_count, T.nilable(Integer), default: nil
  34. # Additional data
  35. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  36. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  37. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  38. # Serialize shared fields
  39. 2 include LogStruct::Log::Interfaces::CommonFields
  40. 2 include LogStruct::Log::Shared::SerializeCommon
  41. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  42. 2 def to_h
  43. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 4 h[LogField::ErrorClass] = error_class
  45. 4 h[LogField::Message] = message
  46. 4 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  47. 4 h[LogField::ErrorContext] = error_context unless error_context.nil?
  48. 4 h[LogField::QueueName] = queue_name unless queue_name.nil?
  49. 4 h[LogField::RetryCount] = retry_count unless retry_count.nil?
  50. 4 h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/sidekiq/log.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../../enums/source"
  14. 2 require_relative "../../enums/event"
  15. 2 require_relative "../../enums/level"
  16. 2 require_relative "../../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class Sidekiq
  20. 2 class Log < T::Struct
  21. 2 extend T::Sig
  22. # Shared/common fields
  23. 2 const :source, Source::Sidekiq, default: Source::Sidekiq
  24. 2 const :event, Event, default: Event::Log
  25. 7 const :timestamp, Time, factory: -> { Time.now }
  26. 2 const :level, Level, default: Level::Info
  27. # Event-specific fields
  28. 2 const :message, T.nilable(String), default: nil
  29. 2 const :context, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  30. 2 const :process_id, T.nilable(Integer), default: nil
  31. 2 const :thread_id, T.nilable(T.any(Integer, String)), default: nil
  32. # Additional data
  33. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  34. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  35. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. 5 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 5 h[LogField::Message] = message unless message.nil?
  43. 5 h[LogField::Context] = context unless context.nil?
  44. 5 h[LogField::ProcessId] = process_id unless process_id.nil?
  45. 5 h[LogField::ThreadId] = thread_id unless thread_id.nil?
  46. 5 h
  47. end
  48. end
  49. end
  50. end
  51. end

lib/log_struct/log/sql.rb

100.0% lines covered

51 relevant lines. 51 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # AUTO-GENERATED: DO NOT EDIT
  4. # Generated by scripts/generate_structs.rb
  5. # Schemas dir: schemas/log_sources/
  6. # Template: tools/codegen/templates/sorbet/event.rb.erb
  7. 2 require "log_struct/shared/interfaces/common_fields"
  8. 2 require "log_struct/shared/interfaces/additional_data_field"
  9. 2 require "log_struct/shared/interfaces/request_fields"
  10. 2 require "log_struct/shared/serialize_common"
  11. 2 require "log_struct/shared/merge_additional_data_fields"
  12. 2 require "log_struct/shared/add_request_fields"
  13. 2 require_relative "../enums/source"
  14. 2 require_relative "../enums/event"
  15. 2 require_relative "../enums/level"
  16. 2 require_relative "../enums/log_field"
  17. 2 module LogStruct
  18. 2 module Log
  19. 2 class SQL < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source::App, default: Source::App
  23. 2 const :event, Event, default: Event::Database
  24. 43 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. # Event-specific fields
  27. 2 const :message, String
  28. 2 const :sql, String
  29. 2 const :name, String
  30. 2 const :duration_ms, Float
  31. 2 const :row_count, T.nilable(Integer), default: nil
  32. 2 const :adapter, T.nilable(String), default: nil
  33. 2 const :bind_params, T.nilable(T::Array[T.untyped]), default: nil
  34. 2 const :database_name, T.nilable(String), default: nil
  35. 2 const :connection_pool_size, T.nilable(Integer), default: nil
  36. 2 const :active_connections, T.nilable(Integer), default: nil
  37. 2 const :operation_type, T.nilable(String), default: nil
  38. 2 const :table_names, T.nilable(T::Array[String]), default: nil
  39. # Additional data
  40. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  41. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  42. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  43. # Serialize shared fields
  44. 2 include LogStruct::Log::Interfaces::CommonFields
  45. 2 include LogStruct::Log::Shared::SerializeCommon
  46. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  47. 2 def to_h
  48. 13 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  49. 13 h[LogField::Message] = message
  50. 13 h[LogField::Sql] = sql
  51. 13 h[LogField::Name] = name
  52. 13 h[LogField::DurationMs] = duration_ms
  53. 13 h[LogField::RowCount] = row_count unless row_count.nil?
  54. 13 h[LogField::Adapter] = adapter unless adapter.nil?
  55. 13 h[LogField::BindParams] = bind_params unless bind_params.nil?
  56. 13 h[LogField::DatabaseName] = database_name unless database_name.nil?
  57. 13 h[LogField::ConnectionPoolSize] = connection_pool_size unless connection_pool_size.nil?
  58. 13 h[LogField::ActiveConnections] = active_connections unless active_connections.nil?
  59. 13 h[LogField::OperationType] = operation_type unless operation_type.nil?
  60. 13 h[LogField::TableNames] = table_names unless table_names.nil?
  61. 13 h
  62. end
  63. end
  64. end
  65. end

lib/log_struct/logged_errors.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Exceptions LogStruct has already logged as a structured line, so an integration that sees the same
  5. # exception again doesn't log it twice. GoodJob passes a failed job's exception to on_thread_error after
  6. # the ActiveJob integration has logged it, for example. Exceptions are held weakly, so a mark doesn't keep
  7. # one alive.
  8. 2 module LoggedErrors
  9. 2 extend T::Sig
  10. 2 @logged = T.let(ObjectSpace::WeakMap.new, ObjectSpace::WeakMap)
  11. 2 @mutex = T.let(Mutex.new, Mutex)
  12. 3 sig { params(error: Exception).void }
  13. 2 def self.mark(error)
  14. 128 @mutex.synchronize { @logged[error] = true }
  15. end
  16. 3 sig { params(error: Exception).returns(T::Boolean) }
  17. 2 def self.logged?(error)
  18. 84 @mutex.synchronize { @logged.key?(error) }
  19. end
  20. end
  21. end

lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb

96.43% lines covered

28 relevant lines. 27 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/tagged_logging"
  4. # Monkey-patch ActiveSupport::TaggedLogging::Formatter to work with structured logging.
  5. #
  6. # Problem: Rails' TaggedLogging prepends tags as text and converts messages to strings.
  7. # When we pass a hash to super(), Rails does "#{tags_text}#{msg}" which calls .to_s
  8. # on the hash, producing Ruby inspect format {message: "..."} instead of valid JSON.
  9. #
  10. # Solution: When LogStruct is enabled, we add tags as a hash entry and delegate to
  11. # LogStruct::Formatter for proper JSON serialization with filtering and standard fields.
  12. 2 module ActiveSupport
  13. 2 module TaggedLogging
  14. 2 extend T::Sig
  15. # Add class-level current_tags method for compatibility with Rails code
  16. 2 sig { returns(T::Array[T.any(String, Symbol)]) }
  17. 2 def self.current_tags
  18. Thread.current[:activesupport_tagged_logging_tags] || []
  19. end
  20. 2 module FormatterExtension
  21. 2 extend T::Sig
  22. 2 extend T::Helpers
  23. 2 requires_ancestor { ::ActiveSupport::TaggedLogging::Formatter }
  24. 3 sig { params(severity: T.any(String, Symbol), time: Time, progname: T.untyped, data: T.untyped).returns(String) }
  25. 2 def call(severity, time, progname, data)
  26. # Preserve original Rails behavior when LogStruct is disabled
  27. 14 return super unless ::LogStruct.enabled?
  28. 12 tags = current_tags
  29. # Add tags to data as hash entry (not text prefix)
  30. 12 data_with_tags = if data.is_a?(Hash)
  31. 1 tags.present? ? data.merge(tags: tags) : data
  32. 11 elsif data.is_a?(::LogStruct::Log::Interfaces::CommonFields) || (data.is_a?(T::Struct) && data.respond_to?(:serialize))
  33. 2 hash = data.serialize
  34. 2 tags.present? ? hash.merge(tags: tags) : hash
  35. else
  36. 9 base = {LogStruct::LogField::Message.serialize => data.to_s}
  37. 9 tags.present? ? base.merge(tags: tags) : base
  38. end
  39. # Delegate to LogStruct::Formatter for JSON serialization with filtering
  40. 12 logstruct_formatter.call(severity, time, progname, data_with_tags)
  41. end
  42. 2 private
  43. 3 sig { returns(::LogStruct::Formatter) }
  44. 2 def logstruct_formatter
  45. 12 @logstruct_formatter ||= T.let(::LogStruct::Formatter.new, T.nilable(::LogStruct::Formatter))
  46. end
  47. end
  48. end
  49. end
  50. 2 ActiveSupport::TaggedLogging::Formatter.prepend(ActiveSupport::TaggedLogging::FormatterExtension)

lib/log_struct/multi_error_reporter.rb

84.62% lines covered

104 relevant lines. 88 lines covered and 16 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "enums/error_reporter"
  4. 2 require_relative "handlers"
  5. # Try to require all supported error reporting libraries
  6. # Users may have multiple installed, so we should load all of them
  7. 2 %w[sentry-ruby bugsnag rollbar honeybadger].each do |gem_name|
  8. 8 require gem_name
  9. rescue LoadError
  10. # If a particular gem is not available, we'll still load the others
  11. end
  12. 2 module LogStruct
  13. # MultiErrorReporter provides a unified interface for reporting errors to various services.
  14. # You can also override this with your own error reporter by setting
  15. # LogStruct#.config.error_reporting_handler
  16. # NOTE: This is used for cases where an error should be reported
  17. # but the operation should be allowed to continue (e.g. scrubbing log data.)
  18. 2 class MultiErrorReporter
  19. # Class variable to store the selected reporter
  20. 2 class CallableReporterWrapper
  21. 2 extend T::Sig
  22. 3 sig { params(callable: T.untyped).void }
  23. 2 def initialize(callable)
  24. 2 @callable = callable
  25. end
  26. 3 sig { returns(T.untyped) }
  27. 2 attr_reader :callable
  28. 2 alias_method :original, :callable
  29. 3 sig { params(error: StandardError, context: T.nilable(T::Hash[Symbol, T.untyped]), source: Source).void }
  30. 2 def call(error, context, source)
  31. 2 case callable_arity
  32. when 3
  33. 1 callable.call(error, context, source)
  34. when 2
  35. 1 callable.call(error, context)
  36. when 1
  37. callable.call(error)
  38. else
  39. callable.call(error, context, source)
  40. end
  41. end
  42. 2 private
  43. 3 sig { returns(Integer) }
  44. 2 def callable_arity
  45. 2 callable.respond_to?(:arity) ? callable.arity : -1
  46. end
  47. end
  48. 4 ReporterImpl = T.type_alias { T.any(ErrorReporter, CallableReporterWrapper) }
  49. 2 @reporter_impl = T.let(nil, T.nilable(ReporterImpl))
  50. 2 class << self
  51. 2 extend T::Sig
  52. 3 sig { returns(ReporterImpl) }
  53. 2 def reporter
  54. 8 reporter_impl
  55. end
  56. # Set the reporter to use (user-friendly API that accepts symbols)
  57. 3 sig { params(reporter_type: T.any(ErrorReporter, Symbol, Handlers::ErrorReporter)).returns(ReporterImpl) }
  58. 2 def reporter=(reporter_type)
  59. 8 @reporter_impl = case reporter_type
  60. when ErrorReporter
  61. reporter_type
  62. when Symbol
  63. 6 resolve_symbol_reporter(reporter_type)
  64. else
  65. 2 wrap_callable_reporter(reporter_type)
  66. end
  67. end
  68. # Auto-detect which error reporting service to use
  69. 3 sig { returns(ErrorReporter) }
  70. 2 def detect_reporter
  71. 1 if defined?(::Sentry)
  72. 1 ErrorReporter::Sentry
  73. elsif defined?(::Bugsnag)
  74. ErrorReporter::Bugsnag
  75. elsif defined?(::Rollbar)
  76. ErrorReporter::Rollbar
  77. elsif defined?(::Honeybadger)
  78. ErrorReporter::Honeybadger
  79. else
  80. ErrorReporter::RailsLogger
  81. end
  82. end
  83. # Report an error to the configured error reporting service
  84. 3 sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
  85. 2 def report_error(error, context = {})
  86. # Call the appropriate reporter method based on what's available
  87. 8 impl = reporter_impl
  88. 8 case impl
  89. when ErrorReporter::Sentry
  90. 2 report_to_sentry(error, context)
  91. when ErrorReporter::Bugsnag
  92. 1 report_to_bugsnag(error, context)
  93. when ErrorReporter::Rollbar
  94. 1 report_to_rollbar(error, context)
  95. when ErrorReporter::Honeybadger
  96. 1 report_to_honeybadger(error, context)
  97. when ErrorReporter::RailsLogger
  98. 1 fallback_logging(error, context)
  99. when CallableReporterWrapper
  100. 2 impl.call(error, context, Source::Internal)
  101. end
  102. end
  103. 2 private
  104. 3 sig { returns(ReporterImpl) }
  105. 2 def reporter_impl
  106. 16 @reporter_impl ||= detect_reporter
  107. end
  108. 3 sig { params(symbol: Symbol).returns(ErrorReporter) }
  109. 2 def resolve_symbol_reporter(symbol)
  110. 6 case symbol
  111. 1 when :sentry then ErrorReporter::Sentry
  112. 2 when :bugsnag then ErrorReporter::Bugsnag
  113. 1 when :rollbar then ErrorReporter::Rollbar
  114. 1 when :honeybadger then ErrorReporter::Honeybadger
  115. 1 when :rails_logger then ErrorReporter::RailsLogger
  116. else
  117. valid_types = ErrorReporter.values.map { |v| ":#{v.serialize}" }.join(", ")
  118. raise ArgumentError, "Unknown reporter type: #{symbol}. Valid types are: #{valid_types}"
  119. end
  120. end
  121. 3 sig { params(callable: T.untyped).returns(CallableReporterWrapper) }
  122. 2 def wrap_callable_reporter(callable)
  123. 2 unless callable.respond_to?(:call)
  124. raise ArgumentError, "Reporter must respond to #call"
  125. end
  126. 2 CallableReporterWrapper.new(callable)
  127. end
  128. # Report to Sentry
  129. 3 sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
  130. 2 def report_to_sentry(error, context = {})
  131. 2 return unless defined?(::Sentry)
  132. # Use the proper Sentry interface defined in the RBI
  133. 2 ::Sentry.capture_exception(error, extra: context)
  134. rescue => e
  135. 1 fallback_logging(e, {original_error: error.class.to_s})
  136. end
  137. # Report to Bugsnag
  138. 3 sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
  139. 2 def report_to_bugsnag(error, context = {})
  140. 1 return unless defined?(::Bugsnag)
  141. 1 ::Bugsnag.notify(error) do |report|
  142. 1 report.add_metadata(:context, context)
  143. end
  144. rescue => e
  145. fallback_logging(e, {original_error: error.class.to_s})
  146. end
  147. # Report to Rollbar
  148. 3 sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
  149. 2 def report_to_rollbar(error, context = {})
  150. 1 return unless defined?(::Rollbar)
  151. 1 ::Rollbar.error(error, context)
  152. rescue => e
  153. fallback_logging(e, {original_error: error.class.to_s})
  154. end
  155. # Report to Honeybadger
  156. 3 sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
  157. 2 def report_to_honeybadger(error, context = {})
  158. 1 return unless defined?(::Honeybadger)
  159. 1 ::Honeybadger.notify(error, context: context)
  160. rescue => e
  161. fallback_logging(e, {original_error: error.class.to_s})
  162. end
  163. # Fallback logging when no error reporting services are available
  164. # Uses the LogStruct.error method to properly log the error
  165. 3 sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
  166. 2 def fallback_logging(error, context = {})
  167. 2 return if error.nil?
  168. # Create a proper error log entry
  169. 2 error_log = Log.from_exception(Source::Internal, error, context)
  170. # Use LogStruct.error to properly log the error
  171. 2 LogStruct.error(error_log)
  172. end
  173. end
  174. end
  175. end

lib/log_struct/param_filters.rb

89.71% lines covered

68 relevant lines. 61 lines covered and 7 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "json"
  4. 2 require_relative "hash_utils"
  5. 2 require_relative "string_scrubber"
  6. 2 require_relative "config_struct/filters"
  7. 2 require_relative "enums/source"
  8. 2 module LogStruct
  9. # This class contains methods for filtering sensitive data in logs
  10. # It is used by Formatter to determine which keys should be filtered
  11. 2 class ParamFilters
  12. 2 class << self
  13. 2 extend T::Sig
  14. # Check if a key should be filtered based on our defined sensitive keys.
  15. # logstruct_field: the key is one of LogStruct's own fields at the top of a line.
  16. # path: the keys from the line's top-level field down to this one (the key alone if not given).
  17. # root: the top-level field's value, or the whole line for a top-level key.
  18. 2 sig do
  19. 2 params(
  20. key: T.untyped,
  21. value: T.untyped,
  22. logstruct_field: T::Boolean,
  23. path: T.nilable(T::Array[String]),
  24. root: T.untyped
  25. ).returns(T::Boolean)
  26. end
  27. 2 def should_filter_key?(key, value = nil, logstruct_field: false, path: nil, root: nil)
  28. 12359 filters = LogStruct.config.filters
  29. 12359 normalized_key = key.to_s
  30. 12359 words = KeyWords.normalize(normalized_key).to_sym
  31. # By its words too, so :api_key filters apiKey and X-Api-Key. Keys in filter_keys_with_hashes are
  32. # filtered too, with a hash of their value.
  33. 12359 [filters.filter_keys, filters.filter_keys_with_hashes].each do |keys|
  34. 24657 return true if keys.include?(normalized_key.downcase.to_sym) || keys.include?(words)
  35. end
  36. 12262 matched = filters.filter_matchers.any? do |matcher|
  37. 24315 next false if logstruct_field && !matcher.matches_logstruct_fields
  38. 14273 matcher.matches?(normalized_key, value)
  39. rescue => e
  40. handle_filter_matcher_error(e, matcher.label, normalized_key)
  41. false
  42. end
  43. 12262 return true if matched
  44. 12165 return false if filters.path_matchers.empty?
  45. 35 path ||= [normalized_key]
  46. 35 root = {} if root.nil?
  47. 35 filters.path_matchers.any? do |matcher|
  48. 35 matcher.matches?(path, value, root)
  49. rescue => e
  50. handle_filter_matcher_error(e, matcher.label, normalized_key)
  51. false
  52. end
  53. end
  54. # Check if a key should be hashed rather than completely filtered
  55. 3 sig { params(key: T.untyped).returns(T::Boolean) }
  56. 2 def should_include_string_hash?(key)
  57. 67 keys = LogStruct.config.filters.filter_keys_with_hashes
  58. 67 keys.include?(key.to_s.downcase.to_sym) || keys.include?(KeyWords.normalize(key.to_s).to_sym)
  59. end
  60. # Convert a value to a filtered summary hash (e.g. { _filtered: { class: "String", ... }})
  61. 3 sig { params(key: T.untyped, data: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
  62. 2 def summarize_json_attribute(key, data)
  63. 86 case data
  64. when Hash
  65. 13 summarize_hash(data)
  66. when Array
  67. 7 summarize_array(data)
  68. when String
  69. 63 summarize_string(data, should_include_string_hash?(key))
  70. else
  71. 3 {_class: data.class}
  72. end
  73. end
  74. # Summarize a filtered String: its class, and an HMAC of it for keys in filter_keys_with_hashes.
  75. # No length, which would say how long a password or token is.
  76. 3 sig { params(string: String, include_hash: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  77. 2 def summarize_string(string, include_hash)
  78. filtered_string = {
  79. 65 _class: String
  80. }
  81. 65 filtered_string[:_hash] = StringScrubber.value_hash(string) if include_hash
  82. 65 filtered_string
  83. end
  84. # Summarize a filtered Hash: its first 10 keys and how many there are. No size, which would say
  85. # how long the secrets in it are.
  86. 3 sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
  87. 2 def summarize_hash(hash)
  88. 16 return {_class: "Hash", _empty: true} if hash.empty?
  89. {
  90. 15 _class: Hash,
  91. _keys_count: hash.keys.size,
  92. 30 _keys: hash.keys.take(10).map { |key| normalize_summary_key(StringScrubber.scrub_name(key)) }
  93. }
  94. end
  95. # Summarize a filtered Array: how many items it has
  96. 3 sig { params(array: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
  97. 2 def summarize_array(array)
  98. 9 return {_class: "Array", _empty: true} if array.empty?
  99. {
  100. 7 _class: Array,
  101. _count: array.size
  102. }
  103. end
  104. 2 private
  105. 3 sig { params(key: T.any(String, Symbol, Integer, T.untyped)).returns(T.any(Symbol, String)) }
  106. 2 def normalize_summary_key(key)
  107. 30 if key.is_a?(Symbol)
  108. 27 key
  109. 3 elsif key.respond_to?(:to_sym)
  110. 2 key.to_sym
  111. else
  112. 1 key.to_s
  113. end
  114. rescue
  115. key.to_s
  116. end
  117. 2 sig { params(error: StandardError, label: String, key: String).void }
  118. 2 def handle_filter_matcher_error(error, label, key)
  119. context = {
  120. matcher: label,
  121. key: key
  122. }
  123. LogStruct.handle_exception(error, source: Source::Internal, context: context)
  124. end
  125. end
  126. end
  127. end

lib/log_struct/params_errors.rb

100.0% lines covered

38 relevant lines. 38 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. # Errors from parsing a request's params: Rails' ParseError (and its ParamErrors), and Rack's. Their messages, and
  5. # those of errors raised because of them (Rails' "Invalid request parameters: ..."), quote the request's input: a
  6. # JSON parser's excerpt of the body, Rack's "invalid %-encoding (<value>)". Nothing can filter a body that didn't
  7. # parse, so LogStruct's lines leave the input out.
  8. 2 module ParamsErrors
  9. 2 extend T::Sig
  10. # Matched by name, so this works without ActionDispatch or Rack loaded
  11. 2 NAMES = T.let(
  12. [
  13. "ActionDispatch::Http::Parameters::ParseError",
  14. "Rack::BadRequest",
  15. "Rack::QueryParser::InvalidParameterError",
  16. "Rack::QueryParser::ParameterTypeError",
  17. "Rack::QueryParser::ParamsTooDeepError"
  18. ].freeze,
  19. T::Array[String]
  20. )
  21. # Rails' message for a ParseError
  22. 2 PARSE_ERROR_MESSAGE = "Error occurred while parsing request parameters"
  23. 2 PARAMS_ERROR_MESSAGE = "Invalid request parameters"
  24. 2 CAUSE_MESSAGE_LEFT_OUT = "(left out: it quotes the request's input)"
  25. # The message and the cause's message to log for an error, without the input a params parser quoted
  26. 3 sig { params(error: Exception).returns([String, T.nilable(String)]) }
  27. 2 def self.messages(error)
  28. 47 cause = error.cause
  29. 47 if params_error?(error)
  30. 7 [parse_error?(error) ? PARSE_ERROR_MESSAGE : PARAMS_ERROR_MESSAGE, nil]
  31. 40 elsif cause && params_error?(cause)
  32. 4 message = error.message.sub(cause.message, "").sub(/[\s:]+\z/, "")
  33. 4 [message.empty? ? PARAMS_ERROR_MESSAGE : message, nil]
  34. else
  35. 36 [error.message, cause&.message]
  36. end
  37. end
  38. # For text that writes an error's whole chain (its causes too): each message in it that quotes a params parser's
  39. # input, with what to write instead. Longest first, since an error's message can contain its cause's. An error
  40. # raised without a message (Rack's and Rails' depth limits) has its class name as its message, which quotes
  41. # nothing and is also the text's label for its class, so it stays.
  42. 3 sig { params(error: Exception).returns(T::Array[[String, String]]) }
  43. 2 def self.replacements(error)
  44. 5 pairs = T.let([], T::Array[[String, String]])
  45. 5 seen = T.let({}.compare_by_identity, T::Hash[Exception, TrueClass])
  46. 5 current = T.let(error, T.nilable(Exception))
  47. 5 while current && !seen.key?(current)
  48. 10 seen[current] = true
  49. 10 message, cause_message = messages(current)
  50. 10 pairs << [current.message, message] if message != current.message && !default_message?(current)
  51. 10 cause = current.cause
  52. 10 pairs << [cause.message, CAUSE_MESSAGE_LEFT_OUT] if cause && cause_message.nil? && !default_message?(cause)
  53. 10 current = cause
  54. end
  55. 16 pairs.reject { |quoted, _| quoted.empty? }.uniq.sort_by { |quoted, _| -quoted.length }
  56. end
  57. 3 sig { params(error: Exception).returns(T::Boolean) }
  58. 2 def self.default_message?(error)
  59. 8 error.message == error.class.name
  60. end
  61. 3 sig { params(error: Exception).returns(T::Boolean) }
  62. 2 def self.parse_error?(error)
  63. 7 !!defined?(::ActionDispatch::Http::Parameters::ParseError) &&
  64. error.instance_of?(::ActionDispatch::Http::Parameters::ParseError)
  65. end
  66. 3 sig { params(error: Exception).returns(T::Boolean) }
  67. 2 def self.params_error?(error)
  68. 660 error.class.ancestors.any? { |ancestor| NAMES.include?(ancestor.name) }
  69. end
  70. end
  71. end

lib/log_struct/rails_boot_banner_silencer.rb

93.44% lines covered

61 relevant lines. 57 lines covered and 4 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "sorbet-runtime"
  4. 2 module LogStruct
  5. 2 module RailsBootBannerSilencer
  6. 2 extend T::Sig
  7. 2 @installed = T.let(false, T::Boolean)
  8. 4 sig { void }
  9. 2 def self.install!
  10. 4 return if @installed
  11. 4 @installed = true
  12. 4 return unless ARGV.include?("server")
  13. # Rails' own banner stays when LogStruct is disabled
  14. 1 return unless LogStruct.enabled?
  15. patch!
  16. end
  17. 3 sig { returns(T::Boolean) }
  18. 2 def self.patch!
  19. begin
  20. 2 require "rails/command"
  21. 1 require "rails/commands/server/server_command"
  22. rescue LoadError
  23. # Best-effort – if Rails isn't available yet we'll try again later
  24. 1 return false
  25. end
  26. 1 server_command = T.let(nil, T.untyped)
  27. # rubocop:disable Sorbet/ConstantsFromStrings
  28. begin
  29. 1 server_command = ::Object.const_get("Rails::Command::ServerCommand")
  30. rescue NameError
  31. server_command = nil
  32. end
  33. # rubocop:enable Sorbet/ConstantsFromStrings
  34. 1 return false unless server_command
  35. 1 patch_server_command(server_command)
  36. 1 true
  37. end
  38. 3 sig { params(server_command: T.untyped).void }
  39. 2 def self.patch_server_command(server_command)
  40. 6 return if server_command <= ServerCommandSilencer
  41. 5 server_command.prepend(ServerCommandSilencer)
  42. end
  43. 2 module ServerCommandSilencer
  44. 2 extend T::Sig
  45. 2 extend T::Helpers
  46. 2 requires_ancestor { Object }
  47. 3 sig { params(args: T.untyped, block: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
  48. 2 def perform(*args, &block)
  49. 1 ::LogStruct.server_mode = true
  50. 1 super
  51. end
  52. 3 sig { params(server: T.untyped, url: T.nilable(String)).void }
  53. 2 def print_boot_information(server, url)
  54. 2 ::LogStruct.server_mode = true
  55. 2 consume_boot_banner(server, url)
  56. end
  57. 2 private
  58. 3 sig { params(server: T.untyped, url: T.nilable(String)).void }
  59. 2 def consume_boot_banner(server, url)
  60. 2 return unless defined?(::LogStruct::Integrations::Puma)
  61. begin
  62. 2 ::LogStruct::Integrations::Puma.emit_boot_if_needed!
  63. rescue => e
  64. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  65. end
  66. begin
  67. 2 model = ::ActiveSupport::Inflector.demodulize(server)
  68. rescue
  69. 1 model = "Puma"
  70. end
  71. lines = [
  72. 2 "=> Booting #{model}",
  73. build_rails_banner_line(url),
  74. "=> Run `#{lookup_executable} --help` for more startup options"
  75. ]
  76. 2 lines.each do |line|
  77. 6 ::LogStruct::Integrations::Puma.process_line(line)
  78. rescue => e
  79. ::LogStruct::Integrations::Puma.handle_integration_error(e)
  80. end
  81. end
  82. 3 sig { params(url: T.nilable(String)).returns(String) }
  83. 2 def build_rails_banner_line(url)
  84. 2 suffix = url ? " #{url}" : ""
  85. 2 "=> Rails #{::Rails.version} application starting in #{::Rails.env}#{suffix}"
  86. rescue
  87. 1 "=> Rails application starting"
  88. end
  89. 3 sig { returns(String) }
  90. 2 def lookup_executable
  91. 3 return "rails" unless respond_to?(:executable, true)
  92. 2 T.cast(send(:executable), String)
  93. rescue
  94. 1 "rails"
  95. end
  96. end
  97. end
  98. end

lib/log_struct/railtie.rb

71.43% lines covered

42 relevant lines. 30 lines covered and 12 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "rails"
  4. 2 require "semantic_logger"
  5. 2 require_relative "formatter"
  6. 2 require_relative "semantic_logger/setup"
  7. 2 require_relative "integrations"
  8. 2 module LogStruct
  9. # Railtie to integrate with Rails
  10. 2 class Railtie < ::Rails::Railtie
  11. # Replace the logger as soon as Rails creates it, so boot-time logs and anything that keeps
  12. # a reference to Rails.logger use LogStruct's logger
  13. 2 initializer "logstruct.configure_logger", after: :initialize_logger do |app|
  14. 3 next unless LogStruct.enabled?
  15. # Use SemanticLogger for powerful logging features
  16. 3 LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
  17. # Initializers that configure Sidekiq log with its logger while config/initializers run
  18. 3 Integrations::Sidekiq.configure_logger if LogStruct.config.integrations.enable_sidekiq
  19. end
  20. # Set up integrations after config/initializers, so that the settings in
  21. # config/initializers/logstruct.rb and filter_parameters added by other initializers apply
  22. 2 initializer "logstruct.setup", after: :load_config_initializers, before: :build_middleware_stack do |app|
  23. # An initializer may have changed `enabled` or `enabled_environments` after the logger was
  24. # (or wasn't) replaced
  25. 3 LogStruct.apply_initializer_settings!
  26. 3 if LogStruct.enabled?
  27. 3 if LogStruct::SemanticLogger::Setup.configured?
  28. # The formatter, colors, noisy logger filter and log file settings in config/initializers apply too
  29. 3 LogStruct::SemanticLogger::Setup.reconfigure_appenders(app)
  30. else
  31. LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
  32. end
  33. elsif LogStruct::SemanticLogger::Setup.configured?
  34. LogStruct::SemanticLogger::Setup.restore_rails_logger(app)
  35. end
  36. 3 unless LogStruct.enabled? && LogStruct.config.integrations.enable_sidekiq
  37. 1 Integrations::Sidekiq.restore_logger
  38. end
  39. 3 Integrations::ActionMailer.setup_fallback_error_handlers(LogStruct.config)
  40. 3 next unless LogStruct.enabled?
  41. # Merge Rails filter parameters into our filters
  42. 3 LogStruct.merge_rails_filter_parameters!
  43. 3 Integrations.setup_integrations(stage: :non_middleware)
  44. end
  45. # Add middleware before the middleware stack is built
  46. 2 initializer "logstruct.configure_middleware", after: "logstruct.setup", before: :build_middleware_stack do |app|
  47. 2 next unless LogStruct.enabled?
  48. 2 Integrations.setup_integrations(stage: :middleware)
  49. end
  50. # Emit Puma lifecycle logs when running `rails server`
  51. 2 initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
  52. 3 next unless LogStruct.enabled?
  53. 2 is_server = ::LogStruct.server_mode?
  54. 2 next unless is_server
  55. begin
  56. require "log_struct/log/puma"
  57. port = LogStruct::Integrations::Puma.port_from_argv(ARGV)
  58. started = LogStruct::Log::Puma::Start.new(
  59. mode: "single",
  60. environment: (defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil,
  61. process_id: Process.pid,
  62. listening_addresses: port ? ["tcp://127.0.0.1:#{port}"] : nil
  63. )
  64. LogStruct.info(started)
  65. at_exit do
  66. shutdown = LogStruct::Log::Puma::Shutdown.new(
  67. process_id: Process.pid
  68. )
  69. LogStruct.info(shutdown)
  70. end
  71. rescue
  72. # best-effort
  73. end
  74. end
  75. # Delegate integration initializers to Integrations module
  76. 2 LogStruct::Integrations.setup_initializers(self)
  77. end
  78. end

lib/log_struct/semantic_logger/color_formatter.rb

84.26% lines covered

108 relevant lines. 91 lines covered and 17 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "semantic_logger"
  4. 2 require_relative "formatter"
  5. 2 module LogStruct
  6. 2 module SemanticLogger
  7. # Writes lines as pretty-printed JSON in color, for reading in a terminal: keys yellow, strings green, numbers
  8. # blue, booleans magenta, nil red and logger names cyan (config.color_map changes them). LogStruct uses it in
  9. # development when LogStruct is enabled there, prefer_json_in_development is false and enable_color_output is
  10. # true (its default). LogStruct entries and Hashes are the JSON line, pretty-printed. Plain lines keep
  11. # SemanticLogger's layout, with their message, exception and tags scrubbed and their named tags filtered as a
  12. # JSON line's ctx is.
  13. 2 class ColorFormatter < ::SemanticLogger::Formatters::Color
  14. 2 extend T::Sig
  15. 3 sig { params(color_map: T.nilable(T::Hash[Symbol, Symbol]), args: T.untyped).void }
  16. 2 def initialize(color_map: nil, **args)
  17. 15 super(**args)
  18. 15 @logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
  19. 15 @json_formatter = T.let(LogStruct::SemanticLogger::Formatter.new, LogStruct::SemanticLogger::Formatter)
  20. # Set up custom color mapping
  21. 15 @custom_colors = T.let(default_color_map.merge(color_map || {}), T::Hash[Symbol, Symbol])
  22. end
  23. 3 sig { override.params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
  24. 2 def call(log, logger)
  25. # Handle LogStruct types specially with colorization
  26. 13 if log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
  27. 6 formatted = format_logstruct_payload(log, logger)
  28. 6 formatted if formatted
  29. else
  30. # For plain messages, use SemanticLogger's default colorization
  31. 7 super
  32. end
  33. end
  34. # The parts of a plain line that hold what was logged, scrubbed like the strings in a JSON line
  35. 3 sig { returns(T.untyped) }
  36. 2 def message
  37. 7 scrub_text(super)
  38. end
  39. 3 sig { returns(T.untyped) }
  40. 2 def payload
  41. 7 scrub_text(super)
  42. end
  43. # SemanticLogger's text for the exception and its causes, without the input a params parser quoted
  44. 3 sig { returns(T.untyped) }
  45. 2 def exception
  46. 7 text = super
  47. 7 error = log.exception
  48. 7 if text.is_a?(String) && error.is_a?(Exception)
  49. 5 ParamsErrors.replacements(error).each { |quoted, logged| text = text.gsub(quoted, logged) }
  50. end
  51. 7 scrub_text(text)
  52. end
  53. 3 sig { returns(T.untyped) }
  54. 2 def tags
  55. 7 scrub_text(super)
  56. end
  57. # Filtered and scrubbed as they are in a JSON line's ctx: a key filtering hides there is hidden here too
  58. 3 sig { returns(T.nilable(String)) }
  59. 2 def named_tags
  60. 7 named_tags = log.named_tags
  61. 7 return if named_tags.nil? || named_tags.empty?
  62. 1 filtered = @logstruct_formatter.process_values({ctx: named_tags.to_h.transform_keys(&:to_sym)})[:ctx]
  63. 3 list = filtered.map { |name, value| "#{color}#{name}: #{value}#{color_map.clear}" }
  64. 1 "{#{list.join(", ")}}"
  65. end
  66. 2 private
  67. 3 sig { params(text: T.untyped).returns(T.untyped) }
  68. 2 def scrub_text(text)
  69. 28 text.is_a?(String) ? StringScrubber.scrub(text) : text
  70. end
  71. 2 sig { returns(LogStruct::Formatter) }
  72. 2 attr_reader :logstruct_formatter
  73. # Default color mapping for LogStruct JSON
  74. 3 sig { returns(T::Hash[Symbol, Symbol]) }
  75. 2 def default_color_map
  76. 15 {
  77. key: :yellow,
  78. string: :green,
  79. number: :blue,
  80. bool: :magenta,
  81. nil: :red,
  82. name: :cyan
  83. }
  84. end
  85. # The line as pretty-printed JSON, with ANSI colors for its keys, strings, numbers, booleans and nulls
  86. 3 sig { params(data: T::Hash[String, T.untyped]).returns(String) }
  87. 2 def colorize_json(data)
  88. 6 json_str = JSON.pretty_generate(data)
  89. # Apply basic colorization with regex
  90. 6 json_str.gsub(/"([^"]+)":/, colorize_text('\1', :key) + ":")
  91. .gsub(/: "([^"]*)"/, ": " + colorize_text('\1', :string))
  92. .gsub(/: (\d+\.?\d*)/, ": " + colorize_text('\1', :number))
  93. .gsub(/: (true|false)/, ": " + colorize_text('\1', :bool))
  94. .gsub(": null", ": " + colorize_text("null", :nil))
  95. end
  96. # The line LogStruct::SemanticLogger::Formatter writes (message, tags and ctx included), in color
  97. 3 sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(T.nilable(String)) }
  98. 2 def format_logstruct_payload(log, logger)
  99. 6 logstruct_json = @json_formatter.call(log, logger)
  100. 6 parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
  101. 6 colorized_json = colorize_json(parsed_data)
  102. 6 prefix = format("%<time>s %<level>s [%<process>s] %<name>s -- ",
  103. time: format_time(log.time),
  104. level: format_level(log.level),
  105. process: log.process_info,
  106. name: format_name(log.name))
  107. # SemanticLogger's appenders add the newline
  108. 6 "#{prefix}#{colorized_json}"
  109. rescue JSON::ParserError
  110. nil
  111. end
  112. # Add ANSI color codes to text
  113. 3 sig { params(text: String, color_type: Symbol).returns(String) }
  114. 2 def colorize_text(text, color_type)
  115. 30 color = @custom_colors[color_type] || :white
  116. 30 "\e[#{color_code_for(color)}m#{text}\e[0m"
  117. end
  118. # Format timestamp
  119. 3 sig { params(time: Time).returns(String) }
  120. 2 def format_time(time)
  121. 13 time.strftime("%Y-%m-%d %H:%M:%S.%6N")
  122. end
  123. # Format log level with color
  124. 3 sig { params(level: T.any(String, Symbol)).returns(String) }
  125. 2 def format_level(level)
  126. 6 level_str = level.to_s.upcase[0]
  127. 6 color = level_color_for(level.to_sym)
  128. 6 "\e[#{color_code_for(color)}m#{level_str}\e[0m"
  129. end
  130. # Format logger name with color
  131. 3 sig { params(name: T.nilable(String)).returns(String) }
  132. 2 def format_name(name)
  133. 6 return "" unless name
  134. 6 color = @custom_colors[:name] || :cyan
  135. 6 "\e[#{color_code_for(color)}m#{name}\e[0m"
  136. end
  137. # Get color for log level
  138. 3 sig { params(level: Symbol).returns(Symbol) }
  139. 2 def level_color_for(level)
  140. 6 case level
  141. when :debug then :magenta
  142. 6 when :info then :cyan
  143. when :warn then :yellow
  144. when :error then :red
  145. when :fatal then :red
  146. else :cyan
  147. end
  148. end
  149. # Get ANSI color code for color symbol
  150. 3 sig { params(color: Symbol).returns(String) }
  151. 2 def color_code_for(color)
  152. 42 case color
  153. when :black then "30"
  154. 8 when :red then "31"
  155. 6 when :green then "32"
  156. 4 when :yellow then "33"
  157. 6 when :blue then "34"
  158. 6 when :magenta then "35"
  159. 12 when :cyan then "36"
  160. when :white then "37"
  161. when :bright_black then "90"
  162. when :bright_red then "91"
  163. when :bright_green then "92"
  164. when :bright_yellow then "93"
  165. when :bright_blue then "94"
  166. when :bright_magenta then "95"
  167. when :bright_cyan then "96"
  168. when :bright_white then "97"
  169. else "37" # default to white
  170. end
  171. end
  172. end
  173. end
  174. end

lib/log_struct/semantic_logger/concerns/log_methods.rb

100.0% lines covered

63 relevant lines. 63 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module SemanticLogger
  5. 2 module Concerns
  6. 2 module LogMethods
  7. 2 extend T::Sig
  8. 2 extend T::Helpers
  9. 2 requires_ancestor { LogStruct::SemanticLogger::Logger }
  10. # Override log methods to handle LogStruct types and broadcast
  11. 4 sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  12. 2 def debug(message = nil, payload = nil, exception = nil, &block)
  13. 14 result = super(*semantic_logger_args(message, payload, exception), &block)
  14. 14 instrument_log(message, :debug)
  15. 14 broadcast_message(:debug, message, block)
  16. 14 result
  17. end
  18. 4 sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  19. 2 def info(message = nil, payload = nil, exception = nil, &block)
  20. 1508 result = super(*semantic_logger_args(message, payload, exception), &block)
  21. 1508 instrument_log(message, :info)
  22. 1507 broadcast_message(:info, message, block)
  23. 1507 result
  24. end
  25. 4 sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  26. 2 def warn(message = nil, payload = nil, exception = nil, &block)
  27. 17 result = super(*semantic_logger_args(message, payload, exception), &block)
  28. 17 instrument_log(message, :warn)
  29. 17 broadcast_message(:warn, message, block)
  30. 17 result
  31. end
  32. 3 sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  33. 2 def error(message = nil, payload = nil, exception = nil, &block)
  34. 78 result = super(*semantic_logger_args(message, payload, exception), &block)
  35. 78 instrument_log(message, :error)
  36. 78 broadcast_message(:error, message, block)
  37. 78 result
  38. end
  39. 3 sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  40. 2 def fatal(message = nil, payload = nil, exception = nil, &block)
  41. 3 result = super(*semantic_logger_args(message, payload, exception), &block)
  42. 3 instrument_log(message, :fatal)
  43. 3 broadcast_message(:fatal, message, block)
  44. 3 result
  45. end
  46. 2 private
  47. # SemanticLogger's arguments for a line: a LogStruct entry or struct goes in the payload, and a Hash is the
  48. # payload itself, so its keys are written at the top level of the line. An exception can come second
  49. # (logger.error({id: 1}, error)) or third.
  50. 4 sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped).returns([T.untyped, T.untyped, T.untyped]) }
  51. 2 def semantic_logger_args(message, payload, exception)
  52. 1620 if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
  53. 237 [nil, {payload: message}, nil]
  54. 1383 elsif message.is_a?(Hash)
  55. 29 [nil, message, payload.is_a?(Exception) ? payload : exception]
  56. else
  57. 1354 [message, payload, exception]
  58. end
  59. end
  60. # Sends a LogStruct entry to the log.logstruct subscribers, after its line is queued to be written, so a
  61. # subscriber's error can't lose it. The error is handled like the app's other errors (standard_errors, which
  62. # raises it from the log call by default), and the lines written while handling it aren't sent to the
  63. # subscribers again.
  64. 4 sig { params(message: T.untyped, level: Symbol).void }
  65. 2 def instrument_log(message, level)
  66. 1620 return unless message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
  67. 237 return if Thread.current[:logstruct_instrumenting_log]
  68. 235 Thread.current[:logstruct_instrumenting_log] = true
  69. begin
  70. 235 ::ActiveSupport::Notifications.instrument("log.logstruct", log: message, level: level)
  71. rescue => e
  72. 2 LogStruct.handle_exception(e, source: Source::App, context: {notification: "log.logstruct"})
  73. ensure
  74. 235 Thread.current[:logstruct_instrumenting_log] = false
  75. end
  76. end
  77. # Other loggers get String messages, and what blocks return, scrubbed like a log line's strings
  78. 4 sig { params(level: Symbol, message: T.untyped, block: T.nilable(T.proc.returns(String))).void }
  79. 2 def broadcast_message(level, message, block)
  80. 1619 broadcasts.each do |logger|
  81. 11 next unless logger.respond_to?(level)
  82. 11 if message.is_a?(String)
  83. 9 logger.public_send(level, StringScrubber.scrub(message))
  84. 2 elsif block
  85. 1 logger.public_send(level) do
  86. # The block type says String, but nothing checks what a block returns
  87. 1 text = T.let(block.call, T.untyped)
  88. 1 text.is_a?(String) ? StringScrubber.scrub(text) : text
  89. end
  90. end
  91. end
  92. end
  93. end
  94. end
  95. end
  96. end

lib/log_struct/semantic_logger/formatter.rb

94.94% lines covered

79 relevant lines. 75 lines covered and 4 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "semantic_logger"
  4. 2 require_relative "../formatter"
  5. 2 require_relative "../params_errors"
  6. 2 module LogStruct
  7. 2 module SemanticLogger
  8. # Writes each SemanticLogger line as LogStruct's JSON: LogStruct entries, Hashes, messages and exceptions, with
  9. # SemanticLogger's tags and named tags (the request ID and the running Sidekiq job's context among them). LogStruct::Formatter
  10. # filters and scrubs every value, within the size limits in config.limits.
  11. 2 class Formatter < ::SemanticLogger::Formatters::Json
  12. 2 extend T::Sig
  13. 4 sig { void }
  14. 2 def initialize
  15. 161 super
  16. 161 @logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
  17. end
  18. 4 sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
  19. 2 def call(log, logger)
  20. # Extract LogStruct from various locations where it might be stored
  21. 1628 logstruct = extract_logstruct(log)
  22. # The request ID, the running Sidekiq job's context and the other named tags (logger.tagged(user_id: 1))
  23. # go in req_id and ctx, and tags (logger.tagged("billing"), config.log_tags, ActiveJob's) in tags
  24. 1628 named_tags = log.named_tags
  25. 1628 request_id = named_tags[:request_id]
  26. 1628 job_context = named_tags[:sidekiq_context]
  27. 1628 context = named_tags.except(:request_id, :sidekiq_context)
  28. 1628 context = context.merge(job_context) if job_context.is_a?(Hash)
  29. metadata = {
  30. 3256 request_id: request_id.is_a?(String) ? request_id : nil,
  31. 1628 context: context.empty? ? nil : context,
  32. 1628 tags: log.tags.empty? ? nil : log.tags
  33. }
  34. 1628 json = if logstruct
  35. # Use our formatter to process LogStruct types directly
  36. 240 @logstruct_formatter.call(log.level, log.time, log.name, logstruct, **metadata)
  37. 1388 elsif log.exception
  38. 9 @logstruct_formatter.call(log.level, log.time, log.name, exception_log(log), **metadata)
  39. 1379 elsif log.payload.is_a?(Hash)
  40. # Process hashes through our formatter
  41. 31 payload = with_message(without_reserved_keys(log.payload), log.message)
  42. 31 @logstruct_formatter.call(log.level, log.time, log.name, payload, **metadata)
  43. 1348 elsif log.payload.is_a?(T::Struct)
  44. @logstruct_formatter.call(log.level, log.time, log.name, log.payload, **metadata)
  45. else
  46. # For plain messages, create a Plain log entry
  47. 1348 message_data = log.payload || log.message
  48. 1348 plain_log = ::LogStruct::Log::Plain.new(
  49. message: message_data,
  50. timestamp: log.time
  51. )
  52. 1348 @logstruct_formatter.call(log.level, log.time, log.name, plain_log, **metadata)
  53. end
  54. # SemanticLogger appenders typically add their own newline. Avoid double newlines by stripping ours.
  55. 1628 json.end_with?("\n") ? json.chomp : json
  56. end
  57. 2 private
  58. # logger.error(exception) and logger.error("Message", exception): an error line with the
  59. # exception's class, message and backtrace. A logged message goes in msg, and the exception's
  60. # message in error_message. A Hash logged with them adds its keys to the line.
  61. 3 sig { params(log: ::SemanticLogger::Log).returns(LogStruct::Log::Error) }
  62. 2 def exception_log(log)
  63. 9 exception = T.let(log.exception, Exception)
  64. 9 message = log.message
  65. 9 logged_message = (message.is_a?(String) && !message.empty?) ? message : nil
  66. 9 payload = log.payload
  67. # Without the input a params parser quoted
  68. 9 exception_message = ParamsErrors.messages(exception).first
  69. 9 LogStruct::Log::Error.new(
  70. source: Source::App,
  71. error_class: exception.class,
  72. message: logged_message || exception_message,
  73. 9 error_message: logged_message ? exception_message : nil,
  74. backtrace: exception.backtrace,
  75. 9 additional_data: payload.is_a?(Hash) ? without_reserved_keys(payload) : nil,
  76. timestamp: log.time
  77. )
  78. end
  79. # A message logged beside a Hash (logger.info("Saved", id: 1)) goes in msg, unless the Hash
  80. # has its own
  81. 3 sig { params(payload: T::Hash[T.untyped, T.untyped], message: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
  82. 2 def with_message(payload, message)
  83. 31 return payload unless message.is_a?(String) && !message.empty?
  84. 3 return payload if payload.key?(:msg) || payload.key?("msg")
  85. 2 {msg: message}.merge(payload)
  86. end
  87. # A logged Hash may set its own src and evt, but the line's timestamp and level come from the logger
  88. 2 HASH_RESERVED_KEYS = T.let(%w[ts lvl].freeze, T::Array[String])
  89. 3 sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) }
  90. 2 def without_reserved_keys(hash)
  91. 135 reserved = hash.keys.select { |key| HASH_RESERVED_KEYS.include?(key.to_s) }
  92. 33 return hash if reserved.empty?
  93. # A copy, since other appenders format the same Hash
  94. 1 copy = hash.dup
  95. 1 reserved.each do |key|
  96. 2 LogStruct.handle_exception(
  97. ArgumentError.new("A logged Hash attempted to set reserved key: #{key}"),
  98. source: Source::Internal
  99. )
  100. 2 copy.delete(key)
  101. end
  102. 1 copy
  103. end
  104. # Extract a LogStruct from the various places it might be stored in a SemanticLogger::Log
  105. 2 sig do
  106. 2 params(log: ::SemanticLogger::Log).returns(
  107. T.nilable(
  108. T.any(
  109. LogStruct::Log::Interfaces::CommonFields,
  110. LogStruct::Log::Interfaces::PublicCommonFields,
  111. T::Struct
  112. )
  113. )
  114. )
  115. end
  116. 2 def extract_logstruct(log)
  117. # Check payload first (most common path for structured logging)
  118. 1628 if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
  119. 238 return T.cast(log.payload[:payload], LogStruct::Log::Interfaces::CommonFields)
  120. end
  121. 1390 if log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
  122. return log.payload
  123. end
  124. 1390 if log.payload.is_a?(LogStruct::Log::Interfaces::PublicCommonFields)
  125. return log.payload
  126. end
  127. # Check message - this is where structs end up when passed directly to logger.info(struct)
  128. 1390 if log.message.is_a?(LogStruct::Log::Interfaces::CommonFields)
  129. return T.cast(log.message, LogStruct::Log::Interfaces::CommonFields)
  130. end
  131. # Check for T::Struct in payload hash (might be a LogStruct struct not implementing CommonFields directly)
  132. 1390 if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
  133. 2 struct = log.payload[:payload]
  134. 2 if struct.respond_to?(:source) && struct.respond_to?(:event)
  135. 2 return struct
  136. end
  137. end
  138. 1388 nil
  139. end
  140. 2 sig { returns(LogStruct::Formatter) }
  141. 2 attr_reader :logstruct_formatter
  142. end
  143. end
  144. end

lib/log_struct/semantic_logger/logger.rb

86.67% lines covered

60 relevant lines. 52 lines covered and 8 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/logger"
  4. 2 require "semantic_logger"
  5. 2 require_relative "concerns/log_methods"
  6. 2 module LogStruct
  7. 2 module SemanticLogger
  8. # Rails.logger's replacement: a SemanticLogger logger that also takes LogStruct's typed entries and Hashes, and
  9. # passes each line on to the loggers added with broadcast_to.
  10. #
  11. # ```ruby
  12. # logger = LogStruct::SemanticLogger::Logger.new("MyApp")
  13. #
  14. # # A typed entry
  15. # logger.info(LogStruct::Log::Plain.new(message: "User authenticated", additional_data: {user_id: 123}))
  16. #
  17. # # A Hash: its keys are written at the top level of the line
  18. # logger.info(action: "user_login", user_id: 123)
  19. #
  20. # # A message, with a Hash or an exception beside it
  21. # logger.info("User logged in", user_id: 123)
  22. # logger.error("Import failed", error)
  23. # ```
  24. 2 class Logger < ::SemanticLogger::Logger
  25. 2 extend T::Sig
  26. 4 sig { params(name: T.any(String, Symbol, Module, T::Class[T.anything]), level: T.nilable(Symbol), filter: T.untyped).void }
  27. 2 def initialize(name = "Application", level: nil, filter: nil)
  28. # SemanticLogger::Logger expects positional arguments, not named arguments
  29. 125 super(name, level, filter)
  30. # T.untyped because users can pass any logger: ::Logger, ActiveSupport::Logger,
  31. # custom loggers (FakeLogger in tests), or third-party loggers
  32. 125 @broadcasts = T.let([], T::Array[T.untyped])
  33. # ActiveJob expects logger.formatter to exist and respond to current_tags
  34. 125 @formatter = T.let(FormatterProxy.new, FormatterProxy)
  35. end
  36. # ActiveSupport::BroadcastLogger compatibility
  37. # These methods allow Rails.logger to broadcast to multiple loggers
  38. 4 sig { returns(T::Array[T.untyped]) }
  39. 2 attr_reader :broadcasts
  40. # ActiveJob compatibility - expects logger.formatter.current_tags
  41. 4 sig { returns(FormatterProxy) }
  42. 2 attr_reader :formatter
  43. # T.untyped for logger param because we accept any logger-like object:
  44. # ::Logger, ActiveSupport::Logger, test doubles, etc.
  45. 3 sig { params(logger: T.untyped).returns(T.untyped) }
  46. 2 def broadcast_to(logger)
  47. # Every SemanticLogger logger writes to the same appenders, so broadcasting to one
  48. # would write each message twice. Sidekiq's Rails integration broadcasts Rails.logger
  49. # to Sidekiq's logger, for example.
  50. 11 return logger if logger.is_a?(::SemanticLogger::Logger)
  51. # `rails server` in development broadcasts to a STDOUT logger, which would print each
  52. # message again as text beside LogStruct's line
  53. 10 return logger if console_logger_with_console_appender?(logger)
  54. 9 @broadcasts << logger
  55. 9 logger
  56. end
  57. 3 sig { params(logger: T.untyped).void }
  58. 2 def stop_broadcasting_to(logger)
  59. 2 @broadcasts.delete(logger)
  60. end
  61. 2 include Concerns::LogMethods
  62. # Support for tagged logging
  63. 3 sig { params(tags: T.untyped, block: T.proc.returns(T.untyped)).returns(T.untyped) }
  64. 2 def tagged(*tags, &block)
  65. 26 tag_array = tags.flatten
  66. 26 if tag_array.empty?
  67. super(&block)
  68. else
  69. # Sorbet can't check a splat of an Array whose size it doesn't know
  70. 26 super(*T.unsafe(tag_array), &block)
  71. end
  72. end
  73. # Ensure compatibility with Rails.logger interface
  74. 2 sig { returns(T::Array[T.any(String, Symbol)]) }
  75. 2 def current_tags
  76. ::SemanticLogger.tags
  77. end
  78. 2 sig { void }
  79. 2 def clear_tags!
  80. # SemanticLogger doesn't have clear_tags!, use pop_tags instead
  81. count = ::SemanticLogger.tags.length
  82. ::SemanticLogger.pop_tags(count) if count > 0
  83. end
  84. 2 sig { params(tags: T.untyped).returns(T::Array[T.untyped]) }
  85. 2 def push_tags(*tags)
  86. flat = tags.flatten.compact
  87. flat.each { |tag| ::SemanticLogger.push_tags(tag) }
  88. flat
  89. end
  90. 2 sig { params(count: Integer).void }
  91. 2 def pop_tags(count = 1)
  92. ::SemanticLogger.pop_tags(count)
  93. end
  94. # Support for << operator (used by RailsLogSplitter). info passes the message on to the broadcast loggers.
  95. 3 sig { params(msg: String).returns(T.self_type) }
  96. 2 def <<(msg)
  97. 2 info(msg)
  98. 2 self
  99. end
  100. 2 private
  101. 3 sig { params(logger: T.untyped).returns(T::Boolean) }
  102. 2 def console_logger_with_console_appender?(logger)
  103. # rails server's logger writes to the STDOUT constant, which differs from $stdout when $stdout was replaced
  104. 10 ::SemanticLogger.appenders.console_output? &&
  105. ::ActiveSupport::Logger.logger_outputs_to?(logger, $stdout, $stderr, STDOUT, STDERR) # rubocop:disable Style/GlobalStdStream
  106. end
  107. end
  108. # Proxy object to provide ActiveJob-compatible formatter interface
  109. # Also implements the standard Logger formatter interface (call method)
  110. # for compatibility with Ruby's Logger (especially logger gem 1.7.0+)
  111. 2 class FormatterProxy < ::Logger::Formatter
  112. 2 extend T::Sig
  113. 3 sig { returns(T::Array[T.any(String, Symbol)]) }
  114. 2 def current_tags
  115. 15 Thread.current[:activesupport_tagged_logging_tags] || []
  116. end
  117. end
  118. end
  119. end

lib/log_struct/semantic_logger/reopen_after_fork.rb

89.47% lines covered

19 relevant lines. 17 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module SemanticLogger
  5. # SemanticLogger writes lines from a background thread, and a forked child doesn't get its parent's threads, so
  6. # lines logged in Puma, Unicorn, Passenger or Pitchfork workers, Resque jobs and other forked children were never
  7. # written. Ruby calls Process._fork for every fork, so each child reopens the appenders (a new writer thread).
  8. 2 module ReopenAfterFork
  9. 2 extend T::Sig
  10. 2 @installed = T.let(false, T::Boolean)
  11. 4 sig { void }
  12. 2 def self.install!
  13. 3 return if @installed
  14. 2 @installed = true
  15. 2 Process.singleton_class.prepend(ForkHook)
  16. end
  17. 2 module ForkHook
  18. 2 extend T::Sig
  19. 3 sig { returns(Integer) }
  20. 2 def _fork
  21. 4 pid = T.let(super, Integer)
  22. 4 if pid.zero?
  23. begin
  24. ::SemanticLogger.reopen
  25. rescue => e
  26. LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :semantic_logger})
  27. end
  28. end
  29. 4 pid
  30. end
  31. end
  32. end
  33. end
  34. end

lib/log_struct/semantic_logger/setup.rb

82.18% lines covered

101 relevant lines. 83 lines covered and 18 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "semantic_logger"
  4. 2 require_relative "formatter"
  5. 2 require_relative "color_formatter"
  6. 2 require_relative "logger"
  7. 2 require_relative "reopen_after_fork"
  8. 2 module LogStruct
  9. # LogStruct writes its lines through SemanticLogger. It replaces Rails.logger and the framework loggers with a
  10. # SemanticLogger logger whose formatter writes LogStruct's JSON lines. SemanticLogger formats and writes each line on
  11. # a background thread, so a request or job doesn't wait for the write, and it can write to several appenders
  12. # (STDOUT, a file, a log service) at once.
  13. 2 module SemanticLogger
  14. # Sets up SemanticLogger for a Rails app: its level, appenders and formatters, and the loggers it replaces
  15. 2 module Setup
  16. 2 extend T::Sig
  17. # Load hooks for the framework components whose loggers LogStruct replaces
  18. 2 COMPONENT_LOAD_HOOKS = T.let(
  19. %i[active_record action_controller action_mailer active_job action_view action_cable].freeze,
  20. T::Array[Symbol]
  21. )
  22. # The logger LogStruct installed, and the loggers it replaced
  23. 2 @logger = T.let(nil, T.nilable(LogStruct::SemanticLogger::Logger))
  24. 2 @original_rails_logger = T.let(nil, T.untyped)
  25. 2 @original_log_subscriber_logger = T.let(nil, T.untyped)
  26. 2 @original_app_logger = T.let(nil, T.untyped)
  27. # The appenders add_appenders added. LogStruct replaces only these, and keeps any an app adds.
  28. 2 @appenders = T.let([], T::Array[T.untyped])
  29. 4 sig { returns(T::Boolean) }
  30. 2 def self.configured?
  31. 2 !@logger.nil?
  32. end
  33. # Configures SemanticLogger and replaces Rails' loggers with LogStruct's:
  34. # - the level from config.log_level (info in production, debug elsewhere)
  35. # - the appenders for the environment (see add_appenders)
  36. # - Rails.logger and the framework loggers (ActiveRecord, ActionController, ActiveJob, ...)
  37. #
  38. # @param app [Rails::Application] The Rails application instance
  39. 4 sig { params(app: T.untyped).void }
  40. 2 def self.configure_semantic_logger(app)
  41. # Set SemanticLogger configuration
  42. 2 ::SemanticLogger.application = Rails.application.class.module_parent_name
  43. 2 ::SemanticLogger.environment = Rails.env
  44. # Determine log level from Rails config
  45. 2 log_level = determine_log_level(app)
  46. 2 ::SemanticLogger.default_level = log_level
  47. # Add appropriate appenders based on environment
  48. 2 remove_appenders
  49. 2 add_appenders(app)
  50. 2 ReopenAfterFork.install!
  51. # Replace Rails.logger with SemanticLogger
  52. 2 replace_rails_logger(app)
  53. end
  54. # Adds the appenders again, with the settings config/initializers changed after the logger was set up
  55. # (prefer_json_in_development, enable_color_output, color_map, filter_noisy_loggers, local_environments).
  56. # Appenders the app added in config/initializers stay.
  57. 4 sig { params(app: T.untyped).void }
  58. 2 def self.reconfigure_appenders(app)
  59. 4 remove_appenders
  60. 4 add_appenders(app)
  61. end
  62. # Removes the appenders add_appenders added, after writing the lines already queued for them
  63. 4 sig { void }
  64. 2 def self.remove_appenders
  65. 6 return if @appenders.empty?
  66. 4 ::SemanticLogger.flush
  67. 10 @appenders.each { |appender| ::SemanticLogger.remove_appender(appender) }
  68. 4 @appenders = []
  69. end
  70. 4 sig { params(app: T.untyped).returns(Symbol) }
  71. 2 def self.determine_log_level(app)
  72. 2 level = if app.config.log_level
  73. 2 app.config.log_level
  74. elsif Rails.env.production?
  75. :info
  76. elsif Rails.env.test?
  77. :debug
  78. else
  79. :debug
  80. end
  81. # Rails config.log_level can be a String or Symbol
  82. 2 level.is_a?(String) ? level.to_sym : level
  83. end
  84. 4 sig { params(app: T.untyped).void }
  85. 2 def self.add_appenders(app)
  86. 11 config = LogStruct.config
  87. # Determine output destination
  88. 11 io = determine_output(app)
  89. # Ruby buffers STDOUT when it isn't a terminal, so in a container lines waited until the buffer
  90. # filled or the process exited. (GoodJob's executable only syncs STDOUT when it writes its own
  91. # text logs there.)
  92. 11 io.sync = true if io.equal?($stdout)
  93. 11 appenders = []
  94. 11 appenders << if Rails.env.development?
  95. 1 if config.prefer_json_in_development
  96. # Default to production-style JSON in development when enabled
  97. ::SemanticLogger.add_appender(
  98. io: io,
  99. formatter: LogStruct::SemanticLogger::Formatter.new,
  100. filter: determine_filter
  101. )
  102. 1 elsif config.enable_color_output
  103. # Opt-in colorful human formatter in development
  104. 1 ::SemanticLogger.add_appender(
  105. io: io,
  106. formatter: LogStruct::SemanticLogger::ColorFormatter.new(
  107. color_map: config.color_map
  108. ),
  109. filter: determine_filter
  110. )
  111. else
  112. ::SemanticLogger.add_appender(
  113. io: io,
  114. formatter: LogStruct::SemanticLogger::Formatter.new,
  115. filter: determine_filter
  116. )
  117. end
  118. else
  119. # Use our custom JSON formatter in non-development environments
  120. 10 ::SemanticLogger.add_appender(
  121. io: io,
  122. formatter: LogStruct::SemanticLogger::Formatter.new,
  123. filter: determine_filter
  124. )
  125. end
  126. # Log to Rails' log file too in development and test (config.local_environments), unless
  127. # RAILS_LOG_TO_STDOUT says not to. Production and other servers log to STDOUT only: nothing
  128. # rotates the file, and in a container it only fills the disk.
  129. 11 local = config.local_environments.include?(Rails.env.to_sym)
  130. 11 if local && app.config.paths["log"].first && ENV["RAILS_LOG_TO_STDOUT"].blank?
  131. 5 appenders << ::SemanticLogger.add_appender(
  132. file_name: app.config.paths["log"].first,
  133. formatter: LogStruct::SemanticLogger::Formatter.new,
  134. filter: determine_filter
  135. )
  136. end
  137. # add_appender returns nil when it ignores a second STDOUT appender
  138. 11 @appenders = appenders.compact
  139. end
  140. 4 sig { params(app: T.untyped).returns(T.untyped) }
  141. 2 def self.determine_output(app)
  142. # Always honor explicit STDOUT directive
  143. 11 return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present?
  144. 6 if Rails.env.test?
  145. # Use StringIO in test to keep stdout clean
  146. 5 StringIO.new
  147. else
  148. # Use STDOUT for app logs in dev/production
  149. 1 $stdout
  150. end
  151. end
  152. 2 NOISY_LOGGERS = /\A(ActionView|ActionController::RoutingError|ActiveRecord::SchemaMigration)/
  153. # With filter_noisy_loggers on, an appender filter that leaves out NOISY_LOGGERS' lines. SemanticLogger
  154. # writes a line when a Proc filter returns true (a Regexp filter would keep only the names it matches).
  155. 4 sig { returns(T.nilable(T.proc.params(log: ::SemanticLogger::Log).returns(T::Boolean))) }
  156. 2 def self.determine_filter
  157. 17 return nil unless LogStruct.config.filter_noisy_loggers
  158. 3 ->(log) { !NOISY_LOGGERS.match?(log.name.to_s) }
  159. end
  160. # Replaces Rails.logger, ActiveSupport::LogSubscriber.logger, config.logger and the framework
  161. # loggers (ActiveRecord, ActionController, ActionMailer, ActiveJob, ActionView, ActionCable)
  162. # with LogStruct's SemanticLogger logger, so every Rails line is a LogStruct JSON line
  163. #
  164. # @param app [Rails::Application] The Rails application instance
  165. 4 sig { params(app: T.untyped).void }
  166. 2 def self.replace_rails_logger(app)
  167. # Create new SemanticLogger instance
  168. 2 logger = LogStruct::SemanticLogger::Logger.new("Rails")
  169. 2 @logger = logger
  170. 2 @original_rails_logger = Rails.logger
  171. 2 @original_log_subscriber_logger = ActiveSupport::LogSubscriber.logger
  172. 2 @original_app_logger = app.config.logger
  173. # Replace Rails.logger
  174. 2 Rails.logger = logger
  175. # Rails assigns ActiveSupport::LogSubscriber.logger once during boot
  176. # (in the active_support.set_log_subscriber_logger initializer), so log
  177. # subscribers such as Lograge keep writing to the original boot logger
  178. # unless this reference is updated along with Rails.logger.
  179. 2 ActiveSupport::LogSubscriber.logger = logger
  180. # Also replace the component loggers. Use load hooks rather than referencing the base
  181. # classes: loading them this early would apply their on_load hooks before the app's
  182. # initializers run, e.g. ActiveRecord would copy an empty filter_parameters into
  183. # filter_attributes.
  184. 2 COMPONENT_LOAD_HOOKS.each do |hook|
  185. 21 ActiveSupport.on_load(hook) { self.logger = logger }
  186. end
  187. # Store reference in app config
  188. 2 app.config.logger = logger
  189. end
  190. # Puts back the loggers that replace_rails_logger replaced. Used when an initializer
  191. # disables LogStruct after the logger was replaced at boot.
  192. 2 sig { params(app: T.untyped).void }
  193. 2 def self.restore_rails_logger(app)
  194. logger = @logger
  195. return unless logger
  196. original_logger = @original_rails_logger
  197. Rails.logger = original_logger
  198. ActiveSupport::LogSubscriber.logger = @original_log_subscriber_logger
  199. app.config.logger = @original_app_logger
  200. remove_appenders
  201. # These hooks run after the ones that installed LogStruct's logger, including for
  202. # components that are already loaded
  203. COMPONENT_LOAD_HOOKS.each do |hook|
  204. ActiveSupport.on_load(hook) do
  205. self.logger = original_logger if self.logger.equal?(logger)
  206. end
  207. end
  208. @logger = nil
  209. end
  210. end
  211. end
  212. end

lib/log_struct/shared/add_request_fields.rb

68.75% lines covered

16 relevant lines. 11 lines covered and 5 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../enums/log_field"
  4. 2 require_relative "interfaces/request_fields"
  5. 2 module LogStruct
  6. 2 module Log
  7. 2 module Shared
  8. 2 module AddRequestFields
  9. 2 extend T::Sig
  10. 2 extend T::Helpers
  11. 2 requires_ancestor { Interfaces::RequestFields }
  12. 2 sig { params(hash: T::Hash[Symbol, T.untyped]).void }
  13. 2 def add_request_fields(hash)
  14. hash[LogField::Path.serialize] = path if path
  15. hash[LogField::HttpMethod.serialize] = http_method if http_method
  16. hash[LogField::SourceIp.serialize] = source_ip if source_ip
  17. hash[LogField::UserAgent.serialize] = user_agent if user_agent
  18. hash[LogField::Referer.serialize] = referer if referer
  19. end
  20. end
  21. end
  22. end
  23. end

lib/log_struct/shared/interfaces/additional_data_field.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # Moved from lib/log_struct/log/interfaces/additional_data_field.rb
  4. 2 module LogStruct
  5. 2 module Log
  6. 2 module Interfaces
  7. 2 module AdditionalDataField
  8. 2 extend T::Sig
  9. 2 extend T::Helpers
  10. 2 interface!
  11. 2 requires_ancestor { T::Struct }
  12. 2 sig { abstract.returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped])) }
  13. 2 def additional_data
  14. end
  15. end
  16. end
  17. end
  18. end

lib/log_struct/shared/interfaces/common_field_base.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../../enums/level"
  4. 2 module LogStruct
  5. 2 module Log
  6. 2 module Interfaces
  7. 2 module CommonFieldBase
  8. 2 extend T::Sig
  9. 2 extend T::Helpers
  10. 2 interface!
  11. 2 sig { abstract.returns(Level) }
  12. 2 def level
  13. end
  14. 2 sig { abstract.returns(Time) }
  15. 2 def timestamp
  16. end
  17. 3 sig { abstract.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  18. 2 def serialize(strict = true)
  19. end
  20. end
  21. end
  22. end
  23. end

lib/log_struct/shared/interfaces/common_fields.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../../enums/source"
  4. 2 require_relative "../../enums/event"
  5. 2 require_relative "../../enums/level"
  6. 2 require_relative "common_field_base"
  7. 2 module LogStruct
  8. 2 module Log
  9. 2 module Interfaces
  10. 2 module CommonFields
  11. 2 extend T::Sig
  12. 2 extend T::Helpers
  13. 2 interface!
  14. 2 include CommonFieldBase
  15. 2 sig { abstract.returns(Source) }
  16. 2 def source
  17. end
  18. 2 sig { abstract.returns(Event) }
  19. 2 def event
  20. end
  21. end
  22. end
  23. end
  24. end

lib/log_struct/shared/interfaces/public_common_fields.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../../enums/level"
  4. 2 require_relative "common_field_base"
  5. 2 module LogStruct
  6. 2 module Log
  7. 2 module Interfaces
  8. 2 module PublicCommonFields
  9. 2 extend T::Sig
  10. 2 extend T::Helpers
  11. 2 interface!
  12. 2 include CommonFieldBase
  13. end
  14. end
  15. end
  16. end

lib/log_struct/shared/interfaces/request_fields.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Log
  5. 2 module Interfaces
  6. 2 module RequestFields
  7. 2 extend T::Sig
  8. 2 extend T::Helpers
  9. 2 interface!
  10. 2 sig { abstract.returns(T.nilable(String)) }
  11. 2 def path
  12. end
  13. 2 sig { abstract.returns(T.nilable(String)) }
  14. 2 def http_method
  15. end
  16. 2 sig { abstract.returns(T.nilable(String)) }
  17. 2 def source_ip
  18. end
  19. 2 sig { abstract.returns(T.nilable(String)) }
  20. 2 def user_agent
  21. end
  22. 2 sig { abstract.returns(T.nilable(String)) }
  23. 2 def referer
  24. end
  25. end
  26. end
  27. end
  28. end

lib/log_struct/shared/merge_additional_data_fields.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "interfaces/additional_data_field"
  4. 2 require_relative "../enums/source"
  5. 2 module LogStruct
  6. 2 module Log
  7. 2 module Shared
  8. 2 module MergeAdditionalDataFields
  9. 2 extend T::Sig
  10. 2 extend T::Helpers
  11. 2 requires_ancestor { T::Struct }
  12. 2 requires_ancestor { Interfaces::AdditionalDataField }
  13. # Reserved keys that cannot be overwritten by additional_data.
  14. # These are the core log structure fields that must not be modified.
  15. 2 RESERVED_KEYS = T.let(%i[src evt lvl ts].freeze, T::Array[Symbol])
  16. 4 sig { params(hash: T::Hash[Symbol, T.untyped]).void }
  17. 2 def merge_additional_data_fields(hash)
  18. 1573 ad = additional_data
  19. 1573 return unless ad
  20. 49 ad.each do |key, value|
  21. 55 sym_key = key.to_sym
  22. 55 if RESERVED_KEYS.include?(sym_key)
  23. 10 LogStruct.handle_exception(
  24. ArgumentError.new("additional_data attempted to overwrite reserved key: #{sym_key}"),
  25. source: Source::Internal
  26. )
  27. 5 next
  28. end
  29. 45 hash[sym_key] = value
  30. end
  31. end
  32. end
  33. end
  34. end
  35. end

lib/log_struct/shared/serialize_common.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../enums/log_field"
  4. 2 require_relative "interfaces/common_fields"
  5. 2 require_relative "merge_additional_data_fields"
  6. 2 module LogStruct
  7. 2 module Log
  8. 2 module Shared
  9. 2 module SerializeCommon
  10. 2 extend T::Sig
  11. 2 extend T::Helpers
  12. 2 requires_ancestor { Interfaces::CommonFields }
  13. 4 sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  14. 2 def serialize(strict = true)
  15. # Start with shared fields (source, event, level, timestamp)
  16. 1718 out = serialize_common(strict)
  17. # Merge event/base fields from the struct-specific hash
  18. 1718 kernel_self = T.cast(self, Kernel)
  19. 1718 field_hash = T.cast(kernel_self.public_send(:to_h), T::Hash[LogStruct::LogField, T.untyped])
  20. 1718 field_hash.each do |log_field, value|
  21. 3321 next if value.nil?
  22. 3321 key = log_field.serialize
  23. # (The formatter cuts a backtrace to config.limits.max_backtrace_lines)
  24. 3321 out[key] = value.is_a?(::Time) ? value.iso8601(3) : value
  25. end
  26. # Merge any additional_data at top level if available
  27. 1718 if kernel_self.respond_to?(:merge_additional_data_fields)
  28. # merge_additional_data_fields expects symbol keys
  29. 1568 merge_target = T.cast(self, LogStruct::Log::Shared::MergeAdditionalDataFields)
  30. 1568 merge_target.merge_additional_data_fields(out)
  31. end
  32. 1714 out
  33. end
  34. 4 sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  35. 2 def serialize_common(strict = true)
  36. {
  37. 1718 LogField::Source.serialize => source.serialize.to_s,
  38. LogField::Event.serialize => event.serialize.to_s,
  39. LogField::Level.serialize => level.serialize.to_s,
  40. LogField::Timestamp.serialize => timestamp.iso8601(3)
  41. }
  42. end
  43. 3 sig { params(options: T.untyped).returns(T::Hash[String, T.untyped]) }
  44. 2 def as_json(options = nil)
  45. 7 serialize.transform_keys(&:to_s)
  46. end
  47. end
  48. end
  49. end
  50. end

lib/log_struct/shared/serialize_common_public.rb

95.65% lines covered

23 relevant lines. 22 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../enums/log_field"
  4. 2 require_relative "interfaces/public_common_fields"
  5. 2 module LogStruct
  6. 2 module Log
  7. # Common serialization for public custom log structs with string/symbol source/event
  8. 2 module SerializeCommonPublic
  9. 2 extend T::Sig
  10. 2 extend T::Helpers
  11. 2 requires_ancestor { Interfaces::PublicCommonFields }
  12. 2 requires_ancestor { Kernel }
  13. 3 sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  14. 2 def serialize_common_public(strict = true)
  15. 5 unless respond_to?(:source) && respond_to?(:event)
  16. raise ArgumentError, "Public log struct must define #source and #event"
  17. end
  18. 5 src_val = public_send(:source)
  19. 5 evt_val = public_send(:event)
  20. 5 src = src_val.respond_to?(:serialize) ? src_val.public_send(:serialize).to_s : src_val.to_s
  21. 5 evt = evt_val.respond_to?(:serialize) ? evt_val.public_send(:serialize).to_s : evt_val.to_s
  22. 5 lvl = level.serialize.to_s
  23. 5 ts = timestamp.iso8601(3)
  24. {
  25. 5 LogField::Source.serialize => src,
  26. LogField::Event.serialize => evt,
  27. LogField::Level.serialize => lvl,
  28. LogField::Timestamp.serialize => ts
  29. }
  30. end
  31. 3 sig { params(options: T.untyped).returns(T::Hash[String, T.untyped]) }
  32. 2 def as_json(options = nil)
  33. 1 serialize.transform_keys(&:to_s)
  34. end
  35. end
  36. end
  37. end

lib/log_struct/sorbet.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. # Note: We use T::Struct for our Log classes so Sorbet is a hard requirement,
  4. # not an optional dependency.
  5. 2 require "sorbet-runtime"
  6. 2 require "log_struct/sorbet/serialize_symbol_keys"
  7. # Don't extend T::Sig to all modules! We're just a library, not a private Rails application
  8. # See: https://sorbet.org/docs/sigs
  9. # class Module
  10. # include T::Sig
  11. # end

lib/log_struct/sorbet/serialize_symbol_keys.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Sorbet
  5. 2 module SerializeSymbolKeys
  6. 2 extend T::Sig
  7. 2 extend T::Helpers
  8. 2 requires_ancestor { T::Struct }
  9. 3 sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  10. 2 def serialize(strict = true)
  11. 4 super.deep_symbolize_keys
  12. end
  13. 2 sig { returns(T::Hash[Symbol, T.untyped]) }
  14. 2 def to_h
  15. serialize
  16. end
  17. end
  18. end
  19. end

lib/log_struct/string_scrubber.rb

98.55% lines covered

138 relevant lines. 136 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "base64"
  4. 2 require "ipaddr"
  5. 2 require "uri"
  6. 2 require_relative "hash_utils"
  7. 2 require_relative "param_filters"
  8. 2 module LogStruct
  9. # StringScrubber is inspired by logstop by @ankane: https://github.com/ankane/logstop
  10. # Enhancements:
  11. # - Shows which type of data was filtered
  12. # - Includes a hash of each filtered email (HashUtils) for request tracing
  13. # - Uses configuration options from LogStruct.config
  14. 2 module StringScrubber
  15. 2 class << self
  16. 2 extend T::Sig
  17. # The password in a URL's user info (postgres://user:password@host, redis://:password@host).
  18. # The user ends at its first colon; the password runs to the last @ before the host.
  19. 2 URL_PASSWORD_REGEX = %r{(//[^\s/:@]*:)[^\s/]+(@)}
  20. # The same in a URL-encoded URL, like https%3A%2F%2Fuser%3Asecret%40example.com
  21. # cspell:ignore Fuser Asecret
  22. 2 ENCODED_URL_PASSWORD_REGEX = %r{(%2F%2F(?:(?!%3A|%2F|%40)[^\s/&])*%3A)(?:(?!%2F)[^\s/&])+(%40)}i
  23. 2 URL_PASSWORD_REPLACEMENT = '\1[PASSWORD]\2'
  24. # A URL's user info that runs past what the scrubbers saw of a long string, where the cut can separate the
  25. # password from the @ that closes it. Everything after the user's colon goes: the password can contain an
  26. # @ of its own (p@ss).
  27. 2 CUT_URL_PASSWORD_REGEX = %r{(//[^\s/:@]*:)[^\s/"'<>]*\z}
  28. 2 CUT_ENCODED_URL_PASSWORD_REGEX = %r{(%2F%2F(?:(?!%3A|%2F|%40)[^\s/&])*%3A)(?:(?!%2F)[^\s/&"'<>])*\z}i
  29. 2 CUT_URL_PASSWORD_REPLACEMENT = '\1[PASSWORD]'
  30. # Basic credentials that run past what the scrubbers saw: a cut token doesn't decode, so it can't be checked
  31. 2 CUT_BASIC_CREDENTIALS_REGEX = %r{\b(Basic\s+)[A-Za-z0-9+/]+=*\z}i
  32. # A URL's query string, for the sensitive params in it
  33. 2 QUERY_STRING_REGEX = /\?([^\s#"'<>]+)/
  34. # OAuth authorization codes, which no key pattern would filter
  35. 2 SENSITIVE_QUERY_PARAMS = T.let(["code"].freeze, T::Array[String])
  36. 2 QUERY_PARAM_REPLACEMENT = "[FILTERED]"
  37. # Letters and digits of any script (an accented name, a domain in another alphabet)
  38. 2 EMAIL_REGEX = /(?<![\p{L}\p{N}._%+-])[\p{L}\p{N}._%+-]+@[\p{L}\p{N}.-]+\.\p{L}{2,}(?![\p{L}\p{N}])/
  39. # A whole String that is an email address
  40. 2 WHOLE_EMAIL_REGEX = /\A#{EMAIL_REGEX}\z/
  41. # The credentials after Bearer or Basic, as in an Authorization header. A Bearer token has at least 16
  42. # characters, and Basic credentials must decode to user:password, so "Bearer token" and "Basic plan" stay.
  43. 2 AUTHORIZATION_CREDENTIALS_REGEX = %r{\b(Bearer|Basic)(\s+)([A-Za-z0-9\-._~+/]+=*)}i
  44. 2 AUTHORIZATION_CREDENTIALS_REPLACEMENT = "[FILTERED]"
  45. # 13 to 19 digits, written together, in fours (4111 1111 1111 1111, with a 19-digit card's last
  46. # three) or in American Express and Diners Club groups (3782 822463 10005). Numbers that fail
  47. # the Luhn check are left alone, so most IDs and timestamps are too.
  48. 2 CREDIT_CARD_REGEX = /
  49. (?<![A-Za-z0-9.-])
  50. (?:
  51. [2-6]\d{12,18}
  52. | [2-6]\d{3}(?<sep>[\ -])\d{4}\k<sep>\d{4}\k<sep>\d{4}(?:\k<sep>\d{3})?
  53. | 3\d{3}(?<sep2>[\ -])\d{6}\k<sep2>\d{4,5}
  54. )
  55. (?![A-Za-z0-9]|-\d)
  56. /x
  57. 2 CREDIT_CARD_REPLACEMENT = "[CREDIT_CARD]"
  58. # US numbers: 555-123-4567, 555 123 4567, 555.123.4567 and (555) 123-4567, with or without +1 or 1
  59. 2 PHONE_REGEX = /(?<![\w+])(?:\+?1[\s.-]?)?(?:\(\d{3}\)[\s.-]?|\d{3}[\s.-])\d{3}[\s.-]\d{4}(?!\w)/
  60. 2 PHONE_REPLACEMENT = "[PHONE]"
  61. # 123-45-6789 and 123 45 6789, but not inside a longer number (9-123-45-6789, 123-45-6789-0)
  62. 2 SSN_REGEX = /(?<!\w|\d-)\d{3}[\s-]\d{2}[\s-]\d{4}(?!\w|-\d)/
  63. 2 SSN_REPLACEMENT = "[SSN]"
  64. 2 IP_REGEX = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
  65. # Text that could be an IPv6 address (2001:db8::1, ::ffff:192.168.1.1, fe80::1%en0 with its zone). IPAddr
  66. # decides whether it is one, and it must have a digit, so Ruby constants (Face::Bead) and times (12:30:45)
  67. # stay.
  68. 2 IPV6_CANDIDATE_REGEX = /(?<![\w:.])(?=[\h:.]*\d)(?:\h{0,4}:){2,7}(?:\d{1,3}(?:\.\d{1,3}){3}|\h{1,4})?(?:%\w+)?(?![\w:])/
  69. 2 IP_REPLACEMENT = "[IP]"
  70. # Six pairs of hex digits, separated by colons or by dashes
  71. 2 MAC_REGEX = /\b[0-9a-f]{2}([:-])[0-9a-f]{2}(?:\1[0-9a-f]{2}){4}\b/i
  72. 2 MAC_REPLACEMENT = "[MAC]"
  73. # Hash keys and Symbols like these are never changed by the scrubbers, so they're skipped
  74. 2 IDENTIFIER_REGEX = /\A[A-Za-z_][A-Za-z0-9_]*\z/
  75. # Scrub sensitive information from a string
  76. 4 sig { params(string: String).returns(String) }
  77. 2 def scrub(string)
  78. 12958 return string if string.empty?
  79. # The scrubbers match letters of any script, which needs valid UTF-8
  80. 12949 string = valid_utf8(string.to_s).dup
  81. 12949 config = LogStruct.config.filters
  82. 12949 scrub_authorization_credentials!(string) if config.authorization_credentials
  83. # Sensitive params in URLs' query strings
  84. 12949 scrub_query_params!(string) if config.url_query_params && string.include?("?")
  85. # Passwords in URLs
  86. 12949 if config.url_passwords
  87. 12945 string.gsub!(URL_PASSWORD_REGEX, URL_PASSWORD_REPLACEMENT)
  88. 12945 string.gsub!(ENCODED_URL_PASSWORD_REGEX, URL_PASSWORD_REPLACEMENT) if string.include?("%")
  89. end
  90. # Emails
  91. 12949 if config.email_addresses
  92. 13249 string.gsub!(EMAIL_REGEX) { |email| "[EMAIL:#{value_hash(email)}]" }
  93. end
  94. # Credit card numbers
  95. 12949 if config.credit_card_numbers
  96. 12943 string.gsub!(CREDIT_CARD_REGEX) do |number|
  97. 24 luhn_valid?(number.delete(" -")) ? CREDIT_CARD_REPLACEMENT : number
  98. end
  99. end
  100. # Phone numbers
  101. 12949 string.gsub!(PHONE_REGEX, PHONE_REPLACEMENT) if config.phone_numbers
  102. # SSNs
  103. 12949 string.gsub!(SSN_REGEX, SSN_REPLACEMENT) if config.ssns
  104. # IPs: IPv6 first, which can end in an IPv4 address
  105. 12949 if config.ip_addresses
  106. 225 scrub_ipv6_addresses!(string) if string.include?(":")
  107. 225 string.gsub!(IP_REGEX, IP_REPLACEMENT)
  108. end
  109. # MAC addresses
  110. 12949 string.gsub!(MAC_REGEX, MAC_REPLACEMENT) if config.mac_addresses
  111. # Custom scrubber
  112. 12949 custom_scrubber = LogStruct.config.string_scrubbing_handler
  113. 12949 string = custom_scrubber.call(string) if !custom_scrubber.nil?
  114. 12949 string
  115. end
  116. # The end of the part of a long string the scrubbers saw (Formatter#scrub_limited_string). They saw only the
  117. # start of a value that runs past it, so a URL password separated from its closing @ (which
  118. # URL_PASSWORD_REGEX needs) and Basic credentials cut short (which no longer decode) are replaced, however
  119. # long they were.
  120. 3 sig { params(string: String).returns(String) }
  121. 2 def scrub_cut_end(string)
  122. 6 filters = LogStruct.config.filters
  123. 6 if filters.url_passwords
  124. 6 string = string.sub(CUT_URL_PASSWORD_REGEX, CUT_URL_PASSWORD_REPLACEMENT)
  125. .sub(CUT_ENCODED_URL_PASSWORD_REGEX, CUT_URL_PASSWORD_REPLACEMENT)
  126. end
  127. 6 if filters.authorization_credentials
  128. 6 string = string.sub(CUT_BASIC_CREDENTIALS_REGEX, "\\1#{AUTHORIZATION_CREDENTIALS_REPLACEMENT}")
  129. end
  130. 6 string
  131. end
  132. # The hash of a value (HashUtils.hash_value). Email addresses are hashed in lowercase, so an address has
  133. # one hash however it's written, and the same hash under a filtered key as in a scrubbed string.
  134. 3 sig { params(value: String).returns(String) }
  135. 2 def value_hash(value)
  136. 346 HashUtils.hash_value(WHOLE_EMAIL_REGEX.match?(value) ? value.downcase : value)
  137. end
  138. # A String as valid UTF-8: binary and other encodings are converted, and bytes that aren't valid
  139. # become U+FFFD
  140. 4 sig { params(string: String).returns(String) }
  141. 2 def valid_utf8(string)
  142. 37885 return string if string.ascii_only? || (string.encoding == Encoding::UTF_8 && string.valid_encoding?)
  143. 4 utf8 = if string.encoding == Encoding::BINARY || string.encoding == Encoding::US_ASCII
  144. 3 string.dup.force_encoding(Encoding::UTF_8)
  145. else
  146. begin
  147. 1 string.encode(Encoding::UTF_8)
  148. rescue EncodingError
  149. string.dup.force_encoding(Encoding::UTF_8)
  150. end
  151. end
  152. 4 utf8.valid_encoding? ? utf8 : utf8.scrub
  153. end
  154. # A Hash key or a Symbol, scrubbed like a String unless it's a plain identifier (user_id,
  155. # createdAt). Returns it unchanged, or the scrubbed String when scrubbing changed it.
  156. 4 sig { params(name: T.untyped).returns(T.untyped) }
  157. 2 def scrub_name(name)
  158. 12334 return name unless name.is_a?(String) || name.is_a?(Symbol)
  159. 12332 string = name.to_s
  160. 12332 return name if IDENTIFIER_REGEX.match?(string)
  161. 9 scrubbed = scrub(string)
  162. 9 (scrubbed == string) ? name : scrubbed
  163. end
  164. # The URL without its query string and fragment (for Referer headers)
  165. 3 sig { params(url: T.nilable(String)).returns(T.nilable(String)) }
  166. 2 def without_query(url)
  167. 23 return url if url.nil?
  168. 4 url.split(/[?#]/, 2).first || ""
  169. end
  170. 2 private
  171. 4 sig { params(string: String).void }
  172. 2 def scrub_ipv6_addresses!(string)
  173. 80 string.gsub!(IPV6_CANDIDATE_REGEX) do |candidate|
  174. # The zone (an interface's name) is replaced with its address
  175. 13 ipv6_address?(candidate.split("%", 2).first.to_s) ? IP_REPLACEMENT : candidate
  176. end
  177. end
  178. 3 sig { params(candidate: String).returns(T::Boolean) }
  179. 2 def ipv6_address?(candidate)
  180. 13 ::IPAddr.new(candidate).ipv6?
  181. rescue ::IPAddr::Error
  182. 3 false
  183. end
  184. # Bearer tokens and Basic credentials (Authorization: Bearer eyJ... becomes Bearer [FILTERED])
  185. 4 sig { params(string: String).void }
  186. 2 def scrub_authorization_credentials!(string)
  187. 12946 return unless string.match?(/bearer|basic/i)
  188. 31 string.gsub!(AUTHORIZATION_CREDENTIALS_REGEX) do |match|
  189. 12 scheme = Regexp.last_match(1).to_s
  190. 12 credentials = Regexp.last_match(3).to_s
  191. 12 if authorization_credentials?(scheme, credentials)
  192. 4 "#{scheme}#{Regexp.last_match(2)}#{AUTHORIZATION_CREDENTIALS_REPLACEMENT}"
  193. else
  194. 8 match
  195. end
  196. end
  197. end
  198. 3 sig { params(scheme: String, credentials: String).returns(T::Boolean) }
  199. 2 def authorization_credentials?(scheme, credentials)
  200. 12 return credentials.length >= 16 if scheme.casecmp?("bearer")
  201. 8 credentials.length >= 8 && Base64.strict_decode64(credentials).include?(":")
  202. rescue ArgumentError
  203. 1 false
  204. end
  205. # Replaces the values of query params whose names would be filtered as keys
  206. # (/reset?token=abc&page=2 becomes /reset?token=[FILTERED]&page=2)
  207. 4 sig { params(string: String).void }
  208. 2 def scrub_query_params!(string)
  209. 18 string.gsub!(QUERY_STRING_REGEX) do
  210. 14 query = Regexp.last_match(1).to_s
  211. 14 params = query.split("&", -1).map do |param|
  212. 22 name, value = param.split("=", 2)
  213. 22 next param if value.nil? || value.empty? || value == QUERY_PARAM_REPLACEMENT
  214. 18 sensitive_query_param?(name.to_s, value) ? "#{name}=#{QUERY_PARAM_REPLACEMENT}" : param
  215. end
  216. 14 "?#{params.join("&")}"
  217. end
  218. end
  219. # user[password] is checked as password, written plainly or URL-encoded
  220. 3 sig { params(name: String, value: String).returns(T::Boolean) }
  221. 2 def sensitive_query_param?(name, value)
  222. decoded = begin
  223. 18 URI.decode_www_form_component(name)
  224. rescue ArgumentError
  225. name
  226. end
  227. 18 key = decoded[/\[([^\[\]]*)\]\z/, 1] || decoded
  228. 18 return false if key.empty?
  229. 18 SENSITIVE_QUERY_PARAMS.include?(key.downcase) || ParamFilters.should_filter_key?(key, value)
  230. end
  231. # The Luhn checksum that every card number passes
  232. 3 sig { params(digits: String).returns(T::Boolean) }
  233. 2 def luhn_valid?(digits)
  234. 24 sum = 0
  235. 24 digits.reverse.each_char.with_index do |char, index|
  236. 379 digit = char.to_i
  237. 379 if index.odd?
  238. 187 digit *= 2
  239. 187 digit -= 9 if digit > 9
  240. end
  241. 379 sum += digit
  242. end
  243. 24 (sum % 10).zero?
  244. end
  245. end
  246. end
  247. end

lib/logstruct.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 require "log_struct"

rails_test_app/logstruct_test_app/Rakefile

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # Add your own tasks in files placed in lib/tasks ending in .rake,
  2. # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
  3. 1 require_relative "config/application"
  4. 1 Rails.application.load_tasks

rails_test_app/logstruct_test_app/app/controllers/application_controller.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 class ApplicationController < ActionController::Base
  4. end

rails_test_app/logstruct_test_app/app/controllers/errors_controller.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. # The app's own error pages, for config.exceptions_app = routes (ErrorPagesTest turns them on). Rails sends the
  4. # request here as GET /<status>.
  5. 1 class ErrorsController < ApplicationController
  6. 1 def show
  7. 3 status = request.path_info.delete_prefix("/").to_i
  8. 3 render json: {status: status}, status: status
  9. end
  10. end

rails_test_app/logstruct_test_app/app/controllers/logging_controller.rb

74.55% lines covered

55 relevant lines. 41 lines covered and 14 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class LoggingController < ApplicationController
  4. # Basic logging
  5. 1 def test_basic
  6. # Test standard Rails logging - this is the primary usage pattern
  7. 12 Rails.logger.info("Info level message")
  8. 12 Rails.logger.warn("Warning level message")
  9. 12 Rails.logger.debug("Debug level message with context")
  10. # For structured data, use LogStruct's Log::Plain
  11. 12 plain_log = LogStruct::Log::Plain.new(
  12. message: "Structured log message",
  13. source: LogStruct::Source::App
  14. )
  15. 12 Rails.logger.info(plain_log)
  16. # Test email scrubbing in plain string
  17. 12 Rails.logger.info("User email is test@example.com and password is secret123")
  18. 12 render json: {status: "ok", message: "Basic logging completed"}
  19. end
  20. # Reads the request's params
  21. 1 def test_params
  22. 1 render json: {keys: params.keys}
  23. end
  24. # Error logging
  25. 1 def test_error
  26. # Since the tests run in the test environment and Rails' test behavior may catch exceptions
  27. # differently, let's log the error but also raise it to ensure it's properly captured
  28. 3 Rails.logger.info("About to raise test error")
  29. begin
  30. 3 raise "Test error for integration testing"
  31. rescue => e
  32. # Log the error first
  33. 3 error_log = LogStruct::Log::Error.new(
  34. source: LogStruct::Source::App,
  35. error_class: e.class,
  36. message: e.message
  37. )
  38. 3 Rails.logger.error(error_log)
  39. # Then re-raise it for the test to catch
  40. 3 raise
  41. end
  42. end
  43. # Custom log structures
  44. 1 def test_custom
  45. # Create and log a custom log structure
  46. 1 5.times do |i|
  47. 5 custom_log = LogStruct::Log::Plain.new(
  48. message: "Custom log message #{i}",
  49. source: LogStruct::Source::App,
  50. additional_data: {
  51. iteration: i,
  52. timestamp: Time.now.to_f,
  53. random: rand(100)
  54. }
  55. )
  56. 5 Rails.logger.info(custom_log)
  57. end
  58. 1 render json: {status: "ok", message: "Custom logging completed"}
  59. end
  60. # Request logging test - DO NOT MODIFY THIS METHOD
  61. # This method INTENTIONALLY reproduces the SystemStackError issue
  62. # which must be fixed in the LogStruct codebase itself.
  63. 1 def test_request
  64. # This is exactly the code that was causing the infinite recursion issue
  65. # We need to fix the library - not modify this test!
  66. 1 request_log = LogStruct::Log::Request.new(
  67. http_method: "GET",
  68. path: "/api/users",
  69. status: 200,
  70. duration_ms: 15.5,
  71. source_ip: "127.0.0.1"
  72. )
  73. 1 Rails.logger.info(request_log)
  74. 1 render json: {status: "ok", message: "Request logging completed"}
  75. end
  76. # Model-related logging
  77. 1 def test_model
  78. # Create a test user to trigger ActiveRecord logging
  79. user = User.create!(name: "Test User", email: "user@example.com")
  80. # Simple string logging
  81. Rails.logger.info("Created user #{user.id}")
  82. # Get the existing user
  83. found_user = User.find(user.id)
  84. Rails.logger.info("Found user: #{found_user.name}")
  85. render json: {status: "ok", message: "Model logging completed", user_id: user.id}
  86. end
  87. # Job-related logging
  88. 1 def test_job
  89. # Enqueue a job to test ActiveJob integration
  90. job = TestJob.perform_later("test_argument")
  91. Rails.logger.info("Job enqueued with ID: #{job.job_id}")
  92. # LogStruct will automatically enhance job enqueued/performed logs
  93. render json: {status: "ok", message: "Job enqueued for testing", job_id: job.job_id}
  94. end
  95. # Context and tagging
  96. 1 def test_context
  97. # TODO: Fix types for the tagged method
  98. # Test Rails' built-in tagged logging
  99. Rails.logger.tagged("REQUEST_ID_123", "USER_456") do
  100. Rails.logger.info("Message with tags")
  101. # Nested tags
  102. Rails.logger.tagged("NESTED") do
  103. Rails.logger.warn("Message with nested tags")
  104. end
  105. end
  106. # Message without tags
  107. Rails.logger.info("Message without tags")
  108. render json: {status: "ok", message: "Context logging completed"}
  109. end
  110. 1 def test_error_logging
  111. # Also test error handling in formatter by logging to trigger fallback handlers
  112. begin
  113. # Raise an error
  114. 1 raise "Test error for recursion safety"
  115. rescue => e
  116. # Log the error, which would trigger the formatter code
  117. 1 Rails.logger.error("Error occurred: #{e.message}")
  118. # Also try structured error logging
  119. 1 error_log = LogStruct::Log::Error.new(
  120. source: LogStruct::Source::App,
  121. message: e.message,
  122. error_class: e.class
  123. )
  124. 1 Rails.logger.error(error_log)
  125. end
  126. # If we got here without a SystemStackError, the infinite recursion was prevented
  127. 1 render json: {status: "ok", message: "Stack-safe error handling test completed"}
  128. end
  129. # AMS serialization - triggers ActiveModelSerializers instrumentation
  130. 1 def test_ams_serialization
  131. 2 user = User.create!(name: "AMS Test User", email: "ams@example.com")
  132. 2 render json: user, serializer: UserSerializer
  133. end
  134. # Shrine file upload - triggers Shrine instrumentation logging
  135. 1 def test_shrine_upload
  136. 5 document = Document.create_with_shrine_file(
  137. filename: "test_document.txt",
  138. content: "This is test content for Shrine upload"
  139. )
  140. 5 render json: {status: "ok", message: "Shrine upload completed", document_id: document.id}
  141. end
  142. end

rails_test_app/logstruct_test_app/app/controllers/security_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class SecurityController < ApplicationController
  4. # The test app is API-only, with no session for protect_from_forgery to check, so this raises the
  5. # error protect_from_forgery raises for a request without a valid token
  6. 1 def submit
  7. 4 raise ActionController::InvalidAuthenticityToken, "Can't verify CSRF token authenticity."
  8. end
  9. # Reading remote_ip runs Rails' IP spoofing check
  10. 1 def remote_ip
  11. 1 render json: {ip: request.remote_ip}
  12. end
  13. end

rails_test_app/logstruct_test_app/app/jobs/application_job.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class ApplicationJob < ActiveJob::Base
  4. end

rails_test_app/logstruct_test_app/app/jobs/test_job.rb

30.0% lines covered

10 relevant lines. 3 lines covered and 7 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class TestJob < ApplicationJob
  4. 1 queue_as :default
  5. 1 def perform(arg)
  6. # Log job processing - standard Rails approach
  7. logger.info("Processing job #{job_id} with argument: #{arg}")
  8. # Simulate some work
  9. sleep 0.1
  10. # Test error handling in a job
  11. begin
  12. raise StandardError, "Test job error"
  13. rescue => e
  14. # Standard Rails logging
  15. logger.error("Job error: #{e.message}")
  16. # Example of enhanced structured logging
  17. exception_log = LogStruct::Log::Error.new(
  18. source: LogStruct::Source::Job,
  19. error_class: e.class,
  20. message: e.message,
  21. additional_data: {job_class: self.class.name, job_id: job_id}
  22. )
  23. logger.error(exception_log)
  24. end
  25. # Log job completion
  26. logger.info("Job #{job_id} completed successfully")
  27. end
  28. end

rails_test_app/logstruct_test_app/app/mailers/application_mailer.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ApplicationMailer < ActionMailer::Base
  2. 1 default from: "from@example.com"
  3. 1 layout "mailer"
  4. end

rails_test_app/logstruct_test_app/app/mailers/test_mailer.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class TestMailer < ApplicationMailer
  4. 1 def test_email_with_ids(account, user)
  5. 1 @account = account
  6. 1 @user = user
  7. 1 mail(to: "test@example.com", subject: "Test Email")
  8. end
  9. 1 def test_email_with_organization(organization)
  10. 1 @organization = organization
  11. 1 mail(to: "test@example.com", subject: "Test Email")
  12. end
  13. end

rails_test_app/logstruct_test_app/app/models/application_record.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class ApplicationRecord < ActiveRecord::Base
  4. 1 primary_abstract_class
  5. 1 self.abstract_class = true
  6. end

rails_test_app/logstruct_test_app/app/models/document.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 class Document < ApplicationRecord
  4. 1 extend T::Sig
  5. # ActiveStorage attachment (used by active_storage_test.rb)
  6. 1 has_one_attached :active_storage_file
  7. # Shrine attachment (used by shrine_logging_test.rb)
  8. 1 include DocumentUploader::Attachment(:file)
  9. 2 sig { params(filename: String, content: String).returns(Document) }
  10. 1 def self.create_with_file(filename:, content:)
  11. 8 document = T.let(create!, Document)
  12. 8 document.active_storage_file.attach(
  13. io: StringIO.new(content),
  14. filename: filename,
  15. content_type: "text/plain"
  16. )
  17. 8 document
  18. end
  19. 2 sig { params(filename: String, content: String).returns(Document) }
  20. 1 def self.create_with_shrine_file(filename:, content:)
  21. 5 io = StringIO.new(content)
  22. 10 io.define_singleton_method(:original_filename) { filename }
  23. 5 document = T.let(new, Document)
  24. 5 document.file = io
  25. 5 document.save!
  26. 5 document
  27. end
  28. end

rails_test_app/logstruct_test_app/app/models/user.rb

81.82% lines covered

11 relevant lines. 9 lines covered and 2 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class User < ApplicationRecord
  4. 1 validates :name, presence: true
  5. 1 validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
  6. # Add callbacks to test logging
  7. 1 after_create :log_creation
  8. 1 after_update :log_update
  9. 1 private
  10. 1 def log_creation
  11. 2 Rails.logger.info("User created with ID: #{id} and email: #{attributes["email"]}")
  12. end
  13. 1 def log_update
  14. # Standard Rails logging with context
  15. changed_attrs = previous_changes.keys.join(", ")
  16. Rails.logger.info("User #{id} updated. Changed attributes: #{changed_attrs}")
  17. end
  18. end

rails_test_app/logstruct_test_app/app/serializers/user_serializer.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class UserSerializer < ActiveModel::Serializer
  4. 1 attributes :id, :name, :email
  5. end

rails_test_app/logstruct_test_app/app/uploaders/document_uploader.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 class DocumentUploader < Shrine
  4. end

rails_test_app/logstruct_test_app/config/application.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # typed: true
  2. 1 require_relative "boot"
  3. 1 require "rails/all"
  4. # Require the gems listed in Gemfile, including any gems
  5. # you've limited to :test, :development, or :production.
  6. 1 Bundler.require(*Rails.groups)
  7. 1 module LogstructTestApp
  8. 1 class Application < Rails::Application
  9. # Initialize configuration defaults for originally generated Rails version.
  10. 1 config.load_defaults 8.2
  11. # Configuration for the application, engines, and railties goes here.
  12. #
  13. # These settings can be overridden in specific environments using the files
  14. # in config/environments, which are processed later.
  15. #
  16. # config.time_zone = "Central Time (US & Canada)"
  17. # config.eager_load_paths << Rails.root.join("extras")
  18. # Only use API mode
  19. 1 config.api_only = true
  20. # Rails' default error pages, or the app's own from its routes (config.exceptions_app = routes), which
  21. # ErrorPagesTest turns on. An unset config.x setting is an empty, truthy OrderedOptions.
  22. 1 config.x.error_pages_from_routes = false
  23. 1 config.exceptions_app = lambda do |env|
  24. 10 exceptions_app = config.x.error_pages_from_routes ? routes : ActionDispatch::PublicExceptions.new(Rails.public_path)
  25. 10 exceptions_app.call(env)
  26. end
  27. # Use test adapter for ActiveJob in all environments for testing
  28. 1 config.active_job.queue_adapter = :test
  29. # Force all environments to log to STDOUT so development behaves like test/production
  30. # This mirrors how many platforms and 12-factor apps expect logs to be emitted.
  31. 1 config.log_level = :debug
  32. 1 stdout_logger = ActiveSupport::Logger.new($stdout)
  33. 1 stdout_logger.formatter = config.log_formatter
  34. 1 config.logger = ActiveSupport::TaggedLogging.new(stdout_logger)
  35. end
  36. end

rails_test_app/logstruct_test_app/config/environment.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # Load the Rails application.
  2. 1 require_relative "application"
  3. # Initialize the Rails application.
  4. 1 Rails.application.initialize!

rails_test_app/logstruct_test_app/config/environments/test.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # The test environment is used exclusively to run your application's
  2. # test suite. You never need to work with it otherwise. Remember that
  3. # your test database is "scratch space" for the test suite and is wiped
  4. # and recreated between test runs. Don't rely on the data there!
  5. 1 Rails.application.configure do
  6. # Host authorization for tests - allow .localhost subdomains, IPs, and www.example.com
  7. 1 config.hosts = [
  8. ".localhost",
  9. "www.example.com",
  10. IPAddr.new("0.0.0.0/0"), # IPv4
  11. IPAddr.new("::/0"), # IPv6
  12. ]
  13. # Settings specified here will take precedence over those in config/application.rb.
  14. # While tests run files are not watched, reloading is not necessary.
  15. 1 config.enable_reloading = false
  16. # Eager loading loads your entire application. When running a single test locally,
  17. # this is usually not necessary, and can slow down your test suite. However, it's
  18. # recommended that you enable it in continuous integration systems to ensure eager
  19. # loading is working properly before deploying your code.
  20. 1 config.eager_load = ENV["CI"].present?
  21. # Configure public file server for tests with cache-control for performance.
  22. 1 config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
  23. # Show full error reports.
  24. 1 config.consider_all_requests_local = true
  25. 1 config.cache_store = :null_store
  26. # Render exception templates for rescuable exceptions and raise for other exceptions.
  27. 1 config.action_dispatch.show_exceptions = :rescuable
  28. # Disable request forgery protection in test environment.
  29. 1 config.action_controller.allow_forgery_protection = false
  30. # Store uploaded files on the local file system in a temporary directory.
  31. 1 config.active_storage.service = :test
  32. # Tell Action Mailer not to deliver emails to the real world.
  33. # The :test delivery method accumulates sent emails in the
  34. # ActionMailer::Base.deliveries array.
  35. 1 config.action_mailer.delivery_method = :test
  36. # Set host to be used by links generated in mailer templates.
  37. 1 config.action_mailer.default_url_options = { host: "example.com" }
  38. # Print deprecation notices to the stderr.
  39. 1 config.active_support.deprecation = :stderr
  40. # Raises error for missing translations.
  41. # config.i18n.raise_on_missing_translations = true
  42. # Annotate rendered view with file names.
  43. # config.action_view.annotate_rendered_view_with_filenames = true
  44. # Raise error when a before_action's only/except options reference missing actions.
  45. 1 config.action_controller.raise_on_missing_callback_actions = true
  46. end

rails_test_app/logstruct_test_app/config/initializers/cors.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Avoid CORS issues when API is called from the frontend app.
  3. # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests.
  4. # Read more: https://github.com/cyu/rack-cors
  5. # Rails.application.config.middleware.insert_before 0, Rack::Cors do
  6. # allow do
  7. # origins "example.com"
  8. #
  9. # resource "*",
  10. # headers: :any,
  11. # methods: [:get, :post, :put, :patch, :delete, :options, :head]
  12. # end
  13. # end

rails_test_app/logstruct_test_app/config/initializers/filter_parameter_logging.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
  3. # Use this to limit dissemination of sensitive information.
  4. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
  5. 1 Rails.application.config.filter_parameters += [
  6. :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
  7. ]

rails_test_app/logstruct_test_app/config/initializers/inflections.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Add new inflection rules using the following format. Inflections
  3. # are locale specific, and you may define rules for as many different
  4. # locales as you wish. All of these examples are active by default:
  5. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  6. # inflect.plural /^(ox)$/i, "\\1en"
  7. # inflect.singular /^(ox)en/i, "\\1"
  8. # inflect.irregular "person", "people"
  9. # inflect.uncountable %w( fish sheep )
  10. # end
  11. # These inflection rules are supported but not enabled by default:
  12. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  13. # inflect.acronym "RESTful"
  14. # end

rails_test_app/logstruct_test_app/config/initializers/logstruct.rb

92.31% lines covered

26 relevant lines. 24 lines covered and 2 lines missed.
    
  1. # typed: strict
  2. 1 require "log_struct"
  3. # Configure LogStruct
  4. 1 LogStruct.configure do |config|
  5. # Specify which environments to enable in
  6. 1 config.enabled_environments = [:development, :test, :production]
  7. # Lets BootLogsIntegrationTest check that `enabled` and `enabled_environments` set here are honored
  8. 1 if ENV.key?("LOGSTRUCT_TEST_INITIALIZER_ENABLED")
  9. config.enabled = ENV["LOGSTRUCT_TEST_INITIALIZER_ENABLED"] == "true"
  10. end
  11. 1 if ENV.key?("LOGSTRUCT_TEST_INITIALIZER_ENVIRONMENTS")
  12. config.enabled_environments = ENV.fetch("LOGSTRUCT_TEST_INITIALIZER_ENVIRONMENTS").split(",").map(&:to_sym)
  13. end
  14. # Specify which environments are considered local/development
  15. 1 config.local_environments = [:development, :test]
  16. # Configure integrations
  17. 1 config.integrations.enable_lograge = true
  18. 1 config.integrations.enable_actionmailer = true
  19. 1 config.integrations.enable_activejob = true
  20. 1 config.integrations.enable_rack_error_handler = true
  21. 1 config.integrations.enable_sidekiq = !!defined?(Sidekiq)
  22. 1 config.integrations.enable_shrine = !!defined?(Shrine)
  23. 1 config.integrations.enable_carrierwave = !!defined?(CarrierWave)
  24. 1 config.integrations.enable_activestorage = true
  25. # Configure string scrubbing filters
  26. 1 config.filters.email_addresses = true
  27. 1 config.filters.url_passwords = true
  28. 1 config.filters.credit_card_numbers = true
  29. 1 config.filters.phone_numbers = true
  30. 1 config.filters.ssns = true
  31. 1 config.filters.ip_addresses = true
  32. 1 config.filters.mac_addresses = true
  33. # Configure error handling modes
  34. 1 config.error_handling_modes.logstruct_errors = LogStruct::ErrorHandlingMode::Log
  35. 1 config.error_handling_modes.security_errors = LogStruct::ErrorHandlingMode::Report
  36. 1 config.error_handling_modes.standard_errors = LogStruct::ErrorHandlingMode::LogProduction
  37. end

rails_test_app/logstruct_test_app/config/initializers/shrine.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 require "shrine"
  4. 1 require "shrine/storage/memory"
  5. # Use memory storage for tests (fast, no filesystem needed)
  6. 1 Shrine.storages = {
  7. cache: Shrine::Storage::Memory.new,
  8. store: Shrine::Storage::Memory.new
  9. }
  10. # Standard Shrine plugins matching DocSpring's production setup
  11. 1 Shrine.plugin :cached_attachment_data
  12. 1 Shrine.plugin :activerecord
  13. # IMPORTANT: This is the key plugin that causes issues.
  14. # A normal Rails app will have this configured BEFORE LogStruct loads.
  15. # LogStruct must handle this gracefully and NOT add a duplicate log_subscriber.
  16. 1 Shrine.plugin :instrumentation

rails_test_app/logstruct_test_app/config/routes.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 Rails.application.routes.draw do
  4. # Testing routes
  5. 1 get "/logging/basic", to: "logging#test_basic"
  6. 1 get "/logging/error", to: "logging#test_error"
  7. 1 get "/logging/model", to: "logging#test_model"
  8. 1 get "/logging/job", to: "logging#test_job"
  9. 1 get "/logging/context", to: "logging#test_context"
  10. 1 get "/logging/custom", to: "logging#test_custom"
  11. 1 get "/logging/request", to: "logging#test_request"
  12. 1 get "/logging/error_logging", to: "logging#test_error_logging"
  13. 1 get "/logging/shrine_upload", to: "logging#test_shrine_upload"
  14. 1 get "/logging/ams", to: "logging#test_ams_serialization"
  15. 1 post "/logging/params", to: "logging#test_params"
  16. 1 post "/security/submit", to: "security#submit"
  17. 1 get "/security/remote_ip", to: "security#remote_ip"
  18. # Error pages, for config.exceptions_app = routes
  19. 5 %w[400 404 422 500].each { |status| match "/#{status}", to: "errors#show", via: :all }
  20. # Healthcheck route
  21. 4 get "/health", to: proc { [200, {}, ["OK"]] }
  22. end

rails_test_app/logstruct_test_app/lib/tasks/logging.rake

50.0% lines covered

6 relevant lines. 3 lines covered and 3 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 1 namespace :logging do
  4. 1 desc "Test log output for rake task logging tests"
  5. 1 task test_output: :environment do
  6. Rails.logger.info "Test log message from rake task"
  7. Rails.logger.tagged("custom_tag") do
  8. Rails.logger.info "Tagged test log message"
  9. end
  10. end
  11. end